Spring Data MongoDB #Indexed(unique = true) in inner field - spring

For example, there are 2 classes:
#Document
public class WordSet {
#Id
private ObjectId id;
private Language language;
private ObjectId languageId;
}
#Document
public class Language {
#Id
private ObjectId id;
#Index(unique = true)
private String languageName;
}
Languages are stored in separate collection and WordSets are stored in another collection. BUT WordSet is stored only with languageId (language is always null when I save WordSet). language field in WordSet is needed, but it retrieved using aggregation
public Flux<WordSet> getAll() {
AggregationOperation[] joinLanguages = new AggregationOperation[] {
lookup(mongoTemplate.getCollectionName(Language.class), "languageId", "_id", "language"),
unwind("language")
};
Aggregation agg = newAggregation(joinLanguages);
return mongoTemplate.aggregate(agg, WordSet.class, WordSet.class);
}
So, WordSet in mongodb contains only languageId and language field is filled using aggregation (join from Language collection).
But if I want to add more than one WordSet to collection I have an error
com.mongodb.MongoWriteException: E11000 duplicate key error collection: langdope.wordSet index: language.language_name_index dup key: { : null }
How can I handle it? I need languageName to be unique, but also I want to save objects which contains Languages without this problems. I can't just mark language as #Transient (aggregation will not work).

Related

Spring JPA CriteriaQuery groupBy based on only one key in a Composite Primary Key (#EmbeddedId)

I am trying to write a criteriaQuery and group results based on emailId, which is one of the keys of a composite PK, embedded into one of my Entity classes.
The method that returns the specification is as follows :
public static Specification < User > getSpecification(Integer id) {
return (root, query, criteriaBuilder) - > {
var predicates = new ArrayList<Predicate()>;
predicates.add(criteriaBuilder.equal(root.get("indexId"), id));
query.groupBy(root.get("details").get("emailId"));
return criteriaBuilder.and(predicates.toArray(new Predicate[0]));
};
}
The entity class:
public class User {
#EmbeddedId private Details details;
private String name;
private String status;
}
#Embeddable
public class Details {
private String emailId;
private Integer branchId;
}
I have skipped some annotations.
I want to group the results in a way where emailId remains unique, even if branchId changes. In essence for data where there are 3 rows of same emailId but different branchId, I should only fetch 1 result.
It seems it throws an error and asks for both components of the composite key to be passed in the query.groupBy statement.
If you could please help me figure out the issue.

Spring specification search inside JSON array of String

I am using Postgres version 12 database and ORM Hibernate mapping, and Assuming that i have next Entity:
#Entity
public class MyEntity {
#Type(type = "json")
#Column(name = "names", columnDefinition = "json")
private List<String> names;
// Getters and Setters
}
So the data will be persisted like that inside the table column (names):
["Sara", "Anton", "Lars"]
and i want to add a specification to search on that json from postgres database, i tried next one but it didn't work :(
private static Specification<MyEntity> withNames(String name) {
return (root, query, builder) -> {
Expression<String> function = builder.function("json_array_elements", String.class, root.get("names"));
return StringUtils.isBlank(name) ? builder.conjunction() : builder.like(function, name + "%");
};
}
Any suggestions of how to get it work?

Order results based on count using spring boot specification API

Consider the entity below.
PS: The model has more fields but for the question to be short I have posted only the relevant fields
Class Employee {
private String name;
private String country;
private String region;
private String department
#OneToMany
private Set<Skill> skills;
}
Class Skill {
private name;
}
I am using spring boot Specification API to filter employees on different fields like region, country, and so on.
public class EmployeeSpec implements Specification<Employee> {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
String fielName = //some field name
String fieldValue = //some field value
switch (fielName ) {
case "country":
return cb.equal(root.get("country"), fieldValue);
case "department":
return cb.equal(root.get("department"), fieldValue);
case "region":
return cb.equal(root.get("region"), fieldValue);
}
}
I want to order the results such that employee with maximum skills comes first. I am not sure how to implement this using Specification.
You can use CriteriaBuilder.size(..). For your case, the code will look like:
cq.orderBy(cb.desc(cb.size(root.get("skills"))));

Find all embedded documents from manual reference in MongoDB

I use MongoDB and Spring Boot in a project. I used manual reference to point out a collection, My structure is as follows:
Reel collection
{
_id : "reel_id_1",
name: "reel 1",
category :[
{
_id : "category_id_1",
name: "category 1",
videos: ["video_id_1","video_id_2"]
}
]
}
Video collection
{
_id: "video_id_1", // first document
name: "mongo"
}
{
_id: "video_id_2", // seconddocument
name: "java"
}
Java classes are
#Document
#Data
public class Reel {
#Id
private ObjectId _id;
private String name;
List<Category> category;
}
#Data
public class Category {
#Id
private ObjectId _id=new ObjectId();
private String name;
Video videos;
}
#Document
#Data
public class Video {
#Id
private ObjectId _id = new ObjectId();
private String name;
}
I tried to join both document via mongoTemplate
public List<Reel> findById(ObjectId _id) {
LookupOperation lookupOperation = LookupOperation.newLookup()
.from("video")
.localField("category.videos")
.foreignField("_id")
.as("category.videos");
UnwindOperation unwindOperation = Aggregation.unwind("category");
Aggregation agg = newAggregation(unwindOperation,match(Criteria.where("_id").is(_id)),lookupOperation);
Aggregation aggregation = newAggregation(lookupOperation);
List<Reel> results = mongoTemplate.aggregate(aggregation, "reel", Reel.class).getMappedResults();
return results;
}
But it throws an error.
Failed to instantiate java.util.List using constructor NO_CONSTRUCTOR with arguments
But since I use "unwind", I created a new Entity UnwindReel and add Category category instead of List<Category> category. And used
List<UnwindReel> results = mongoTemplate.aggregate(aggregation, "reel", UnwindReel.class).getMappedResults();
It combines only first video (video_id_1) object. How can I get all objects inside videos array? Is there any method to fetch?
Your JSON stored in database has wrong structure. Your Reel class expects list of Category, but in database you have stored as nested object.
You need to add this stage just after $lookup
{
"$addFields": {
"category": {
"$map": {
"input": "$category.videos",
"in": {
"videos": "$$this"
}
}
}
}
}
Java code
public List<Reel> findById(String _id) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("_id").is(_id)),
Aggregation.lookup(mongoTemplate.getCollectionName(Video.class), "category.videos", "_id", "category.videos"),
new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$addFields",
new Document("category", new Document("$map", new Document("input", "$category.videos")
.append("in", new Document("videos", "$$this")))));
}
})
.withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
LOG.debug(
aggregation.toString().replaceAll("__collection__", mongoTemplate.getCollectionName(Reel.class)));
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Reel.class), Reel.class)
.getMappedResults();
}
Recomendations
Do not hard-code collection name, use better mongoTemplate.getCollectionName method
Always log aggregation pipeline before performing, it helps debugging.
If your collection will grow in the future, use {allowDiskUse: true} MongoDb aggregation option.

spring-data-jdbc: query entity containing 1-n relation with JOOQ

I am trying to load entities containing a reference to another entity (1-n) with the help of JOOQ (based on spring-data-jdbc).
I'm started extending the spring-data-jdbc-jooq-example.
The adjusted model with the 1-n relation:
#Data
public class Category {
private #Id Long id;
private String name, description;
private AgeGroup ageGroup;
private Set<SubCategory> subCategories;
public Category() {}
public Category(Long id, String name, String description, AgeGroup ageGroup) {
this(id, name, description, ageGroup, new HashSet<>());
}
public Category(Long id, String name, String description, AgeGroup ageGroup, Set<SubCategory> subCategories) {
this.id = id;
this.name = name;
this.description = description;
this.ageGroup = ageGroup;
this.subCategories = subCategories;
}
}
#Data
#AllArgsConstructor
#NoArgsConstructor
public class SubCategory {
private #Id Long id;
private String title;
}
I wrote two queries, one via the #Query-Annotation in the CrudRepository and one with the help of JOOQ in the JooqRepository.
interface CategoryRepository extends CrudRepository<Category, Long>, JooqRepository {
#Query("SELECT * FROM category")
List<Category> findAllWithQuery();
}
public interface JooqRepository {
List<Category> findAllWithJooq();
}
public class JooqRepositoryImpl implements JooqRepository {
private final DSLContext dslContext;
public JooqRepositoryImpl(DSLContext dslContext) {
this.dslContext = dslContext;
}
#Override
public List<Category> findAllWithJooq() {
return dslContext.select()
.from(CATEGORY)
.fetchInto(Category.class);
}
}
(for me both methods should return the same result-set b/c they execute the same query?!)
But my unit-test fails:
#Test
public void exerciseRepositoryForSimpleEntity() {
// create some categories
SubCategory sub0 = new SubCategory(null, "sub0");
SubCategory sub1 = new SubCategory(null, "sub1");
Category cars = new Category(null, "Cars", "Anything that has approximately 4 wheels", AgeGroup._3to8, Sets.newLinkedHashSet(sub0, sub1));
// save category
repository.saveAll(asList(cars));
// execute
List<Category> actual = repository.findAllWithJooq();
List<Category> compare = repository.findAllWithQuery();
Output.list(actual, "JOOQ");
Output.list(compare, "Query");
// verify
assertThat(actual).as("same size of categories").hasSize(compare.size());
assertThat(actual.get(0).getSubCategories()).as("same size of sub-categories").hasSize(compare.get(0).getSubCategories().size());
}
with
java.lang.AssertionError: [same size of sub-categories]
Expecting actual not to be null
As you can see in the following output the sub-categories queried by JOOQ will not be loaded:
2019-11-26 16:28:00.749 INFO 18882 --- [ main] example.springdata.jdbc.jooq.Output : ==== JOOQ ====
Category(id=1,
name=Cars,
description=Anything that has approximately 4 wheels,
ageGroup=_3to8,
subCategories=null)
2019-11-26 16:28:00.749 INFO 18882 --- [ main] example.springdata.jdbc.jooq.Output : ==== Query ====
Category(id=1,
name=Cars,
description=Anything that has approximately 4 wheels,
ageGroup=_3to8,
subCategories=[SubCategory(id=1,
title=sub0),
SubCategory(id=2,
title=sub1)])
This is the used database-shema:
CREATE TABLE IF NOT EXISTS category (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(100),
description VARCHAR(2000),
age_group VARCHAR(20)
);
CREATE TABLE IF NOT EXISTS sub_category (
id INTEGER IDENTITY PRIMARY KEY,
title VARCHAR(100),
category INTEGER
)
In the JOOQ variant, JOOQ does the conversion from ResultSet to object instances. Since JOOQ doesn't know about the interpretation of aggregates as it is done by Spring Data JDBC it only hydrates the Category itself, not the contained Set of SubCategory.
Spring Data JDBC on the other hand interprets the structure of the Category and based on that executes another statement to load the subcategories.

Resources