What is the best practice to store a specification of an Entity in Spring? - spring

My database contain products in a single table called Product, and each product might have certain fields with their specification, e.g.
//Bicycle product
{
"name" : "Bicycle",
"category" : "Bicycles"
"specification" : {
"color" : "red"
}
}
//Wheel product
{
"name" : "Wheel",
"category" : "Spare Parts",
"specification" : {
"diameter" : "7.5"
}
}
So i've come up with idea of making a field of type Map<String, String> (which creates a another table called specifications) in my Product entity to contain those specifications. But i don't like this approach, because all of the additional fields would be of String type, and because Spring will create a bean out of this field, I wont be able to specify the type of value as an abstract class (like this Map<String, Object>).
I thought of creating additional fields, like this:
#ElementCollection
private Map<String, String> string_features;
#ElementCollection
private Map<String, Double> double_features;
// ...
But it looks kind of ugly and I think there is a better way to do it. And also, if the specification field is of a different Entity type, I will have to create another map for that specific entity, e.g.
//Bicycle product
{
"name" : "Bicycle",
"category" : "Bicycles"
"specification" : {
"color" : "red",
"wheel" : {
"name" : "Wheel",
}
}
}

If the value can be only be numbers and strings, maybe you can save the value as strings and then use a regex to check if the string is a number before returning the value.
Otherwise, you need a way to recognize the type.
I think I would change it to this:
//Bicycle product
{
"name" : "Bicycle",
"category" : "Bicycles"
"specifications" : [
{ name: "color", value: "red", type: "string"},
{ name: "diameter", value: "7.5", type: "double"},
...
]
}
You can map it as:
#ElementCollection
private List<Specification> specifications;
...
#Embaddable
class Specification {
String name;
String value;
String type;
// ... getter/setter and everything else
#Transient
Object getConvertedValue() {
if ("double".equals(type)) {
return Double.parse(value);
}
// String is the default
return value;
}
}
The nice thing is that you can have as many types as you want.

Related

Spring Data Mongo: Compare Two Dates in the Same Document

A brief overview of the document I am working with:
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
#Data
#SuperBuilder(toBuilder = true)
public class BreachBrand {
#Id
private String id;
#CreatedDate
#Field("created_date")
#DiffIgnore
private Instant createdDate;
#LastModifiedDate
#Field("last_modified_date")
#DiffIgnore
private Instant lastModifiedDate;
}
What I am trying to do is compare the lastModifiedDate to the createdDate. So I created a criteria object like so:
criteria.add(Criteria.where("last_modified_date").gt("ISODate('created_date')"));
I've also tried:
criteria.add(Criteria.where("last_modified_date").gt("created_date"));
which is then used in the match operation of an Aggregation object. Using the first criteria code snippet, the aggregation looks like this:
{ "aggregate" : "__collection__", "pipeline" : [{ "$lookup" : { "from" : "brands", "localField" : "brand_dfp_id", "foreignField" : "dfp_id", "as" : "brand"}}, { "$match" : { "$and" : [{ "last_modified_date" : { "$gt" : "ISODate('created_date')"}}]}}, { "$sort" : { "date" : -1}}, { "$skip" : 0}, { "$limit" : 25}], "allowDiskUse" : true, "collation" : { "locale" : "en", "strength" : 1}}
The mongoTemplate object executes the aggregate method w/o error but no records are returned.
I'm suspecting that the gt(Object o) method is expecting an object that is an actual value to use to compare against. All is good when I use an actual date:
criteria.add(Criteria.where("last_modified_date").gt(Instant.parse("2019-05-18T17:07:25.333+00:00")));
As an interesting aside the following works in mongoshell:
db.breaches.find({$where: "this.last_modified_date>this.created_date"}).pretty();
And the following works in Compass (but the export to language button will not display the output):
/**
* $match operation
*/
{
last_modified_date: {$gt: ISODate('created_date')}
}
EDIT:
It appears I need to use a projection to determine if last_modified_date is greater than created date. I got this to work in compass:
[{
$project: {
greater: {
$gt: [
'$last_modified_date',
'$created_date'
]
},
doc: '$$ROOT'
}
}, {
$match: {
greater: true
}
}]
I'm having issues moving that into a projection though:
ProjectionOperation projectionOperation = project("last_modified_date", "created_date").andExpression("$gt", "$last_modified_date", "$created_date").as("greater");
I've also tried this:
ProjectionOperation projectionOperation = project("last_modified_date", "created_date").andExpression("$gt", Arrays.asList("$last_modified_date", "$created_date")).as("greater");
Results in an exception when creating the aggregation:
Aggregation aggregation = newAggregation(
lookup("brands", "brand_dfp_id", "dfp_id", "brand"),
projectionOperation,
matchOperation, //Criteria.where("greater").is(true)
sortOperation,
skipOperation,
limitOperation
)
.withOptions(AggregationOptions.builder()
.allowDiskUse(true)
.collation(Collation.of("en").strength(Collation.ComparisonLevel.primary())).build());
exception:
java.lang.IllegalArgumentException: Invalid reference 'date'!
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:114)
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:86)
at org.springframework.data.mongodb.core.aggregation.SortOperation.toDocument(SortOperation.java:74)
at org.springframework.data.mongodb.core.aggregation.AggregationOperation.toPipelineStages(AggregationOperation.java:55)
at org.springframework.data.mongodb.core.aggregation.AggregationOperationRenderer.toDocument(AggregationOperationRenderer.java:56)
at org.springframework.data.mongodb.core.aggregation.AggregationPipeline.toDocuments(AggregationPipeline.java:77)
at org.springframework.data.mongodb.core.aggregation.Aggregation.toPipeline(Aggregation.java:705)
at org.springframework.data.mongodb.core.AggregationUtil.createPipeline(AggregationUtil.java:95)
at org.springframework.data.mongodb.core.MongoTemplate.doAggregate(MongoTemplate.java:2118)
at org.springframework.data.mongodb.core.MongoTemplate.aggregate(MongoTemplate.java:2093)
at org.springframework.data.mongodb.core.MongoTemplate.aggregate(MongoTemplate.java:1992)

Spring Mongo Aggregation give a conversion error

I'm trying to use Mongo aggregation but I receive an error that I don't understand.
This is my domain:
#Document(collection = "tapes")
public class Tape {
#Id
private String id;
private String area;
private Integer tape;
private String tapeModel;
// follow getters e setters
The mongo shell command and the output is the following:
> db.tapes.aggregate([{ $group: { _id: { "area":"$area"}, tapes: {$push: {tape: "$tape"}}}} ])
{ "_id" : { "area" : "free" }, "tapes" : [ { "tape" : 1 }, { "tape" : 2 } ] }
{ "_id" : { "area" : "Qnap" }, "tapes" : [ { "tape" : 3 } ] }
The following is an attempt to re-create the aggregation in Spring:
AggregationOperation group = Aggregation.group("area").push("tape").as("tape");
Aggregation aggregation = Aggregation.newAggregation(group);
AggregationResults<Tape> results = mongoTemplate.aggregate(aggregation, "tapes", Tape.class);
//List<Tape> tapes = mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Tape.class), Tape.class).getMappedResults();
List<Tape> tapes = results.getMappedResults();
System.out.println(tapes);
But I obtain the following error:
Cannot convert [3] of type class java.util.ArrayList into an instance of class java.lang.Integer! Implement a custom Converter<class java.util.ArrayList, class java.lang.Integer> and register it with the CustomConversions. Parent object was: it.unifi.cerm.cermadminspring.domain.Tape#2b84da07 -> null
org.springframework.data.mapping.MappingException: Cannot convert [3] of type class java.util.ArrayList into an instance of class java.lang.Integer! Implement a custom Converter<class java.util.ArrayList, class java.lang.Integer> and register it with the CustomConversions. Parent object was: it.unifi.cerm.cermadminspring.domain.Tape#2b84da07 -> null
I don't understand why, I searched for aggregation examples and all are more or less similar to mine.
Someone can help me?
First, for making life easier, a more simplified aggregation can be used:
> db.tapes.aggregate([ {$group: {_id: "$area", tapes: {$push: "$tape"}}} ])
Which should yield:
{ "_id" : "free", "tapes" : [ 1 , 2 ] }
{ "_id" : "Qnap", "tapes" : [ 3 ] }
Which should be matched by a change to the Java group aggregation operation:
AggregationOperation group = Aggregation.group("area").push("tape").as("tapes");
Note that I've changed to plural: as("tapes")
Then, notice that you are actually returning a document which doesn't have the same structure as the one you've mapped in the Tape class. That document contains two fields, the String id and a List<Integer> tapes fields.
This is the reason for the shorthand group aggregation I've suggested above, for making mapping easier:
public class TapesForArea {
private String id; // which is the area
private List<Integer> tapes;
// getters, setters ...
}
You don't need to map this class using spring-data-mongodb annotations.
Finally, have the aggregation results return the right type:
AggregationResults<TapesForArea> results =
mongoTemplate.aggregate(aggregation, "tapes", TapesForArea.class);
List<TapesForArea> tapes = results.getMappedResults();
BTW, the error comes from the fact that you try to map the single item tape array [ 3 ] into private Integer tape; property of Tape class.

Spring MongoDB: Projecting Array field size if it exists

I have an Array field replies in my document, which may or may not exist. If it exists, I want to return its size otherwise return 0. Below is my projection code. For documents that contain replies field with elements in it, it is returning 0.
ProjectionOperation project = Aggregation.project("title", "datePosted", "likes").
and(ConditionalOperators.ifNull(ArrayOperators.arrayOf("replies").length()).then(0)).as("repliesCount");
Reading the MongoDB reference doc, it is pretty straight forward but somehow it is not working.
Any suggestions are welcome.
Sample Data
{
"_id" : ObjectId("5e9e3873d022d154c54c2969"),
"title" : "Is this a nice place to meet some interesting people?",
"datePosted" : ISODate("2020-04-21T00:04:03.731Z"),
"views" : 0,
"likes" : 0,
"active" : true
},
{
"_id" : ObjectId("5e9e2f37d022d154c54c2961"),
"title" : "I am posting a quick discussion",
"datePosted" : ISODate("2020-04-20T23:24:39.768Z"),
"views" : 120,
"likes" : 4,
"active" : true,
"replies" : [
{
"_id" : ObjectId("5e9e2f69d022d154c54c2963"),
"reply" : "This is the first reply",
"datePosted" : ISODate("2020-04-20T23:25:29.608Z"),
"likes" : 0
},
{
"_id" : ObjectId("5e9e2f69d022ad4c54c2964"),
"reply" : "This is another reply",
"datePosted" : ISODate("2020-04-20T23:25:29.608Z"),
"likes" : 0
}
]
}
Root Cause
The solution provided by #valijon is correct. The problem lies in the POJO DiscussionDoc to which the result is mapped.
#Document(collection = "discussions")
public class DiscussionDoc {
#Id
private ObjectId id;
private String title;
private LocalDateTime datePosted;
private int views;
private int likes;
#Transient
private int repliesCount;
private boolean active;
#Field(value = "replies")
private List<ReplyDoc> replyList;
}
Removing #Transient it works. I am using #Transient on repliesCount because I do not want to persist this field when persisting a DiscussionDoc. There is no usefulness of persisting this field. But it will be used to hold total reply count when fetching a DiscussionDoc. But #Transient is somehow not letting repliesCount be set when a DiscussionDoc is fetched. I have removed #Transient to fix the problem. But what to do about repliesCount field now being persisted in DB?
You were almost close:
ProjectionOperation project = Aggregation.project("title", "datePosted", "likes").
and(ArrayOperators.arrayOf(ConditionalOperators.ifNull("replies").then(Collections.emptyList())).length()).as("repliesCount");
Equivalent to:
{
"$project": {
"title": 1,
"datePosted": 1,
"likes": 1,
"repliesCount": {
"$size": {
"$ifNull": [
"$replies",
[]
]
}
}
}
}
Workaround: You may use Gson serializer to get this calculated value:
private static final Gson g = new GsonBuilder().create();
...
List<Document> result = mongoTemplate.aggregate(aggregation,
mongoTemplate.getCollectionName(DiscussionDoc.class), Document.class)
.getMappedResults();
return result.stream()
.map(doc -> g.fromJson(doc.toJson(), DiscussionDoc.class))
.collect(Collectors.toList());

How does GraphQL support AND-OR query conditions?

I'm new to GraphQL. There is a requirement that query all the qualified data from CUSTOMERS table, which meets condition: GENDER == 'MALE' or 'AGE' >= 20. What GQL should looks like for it?
Someone proposed a similar thing before but it is rejected. That means GraphQL does not support it natively and you have to roll it out by yourself.
Several ways to do it based on what I see :
(1) Define your own query language such as what stackoverflow or Shopify does :
type Query{
customers (query:String) : [Customer]
}
The query becomes :
{
customers (query : "GENDER == 'MALE' or 'AGE' >= 20"){
id
name
}
}
(2) Define your own input object models that can cover all the required searching requirement.Prisma try to define one in OpenCRUD specification .You may take a look on it for the idea . For example , you can define an input model like :
input CustomerFilter {
AND : [CustomerFilter]
OR : [CustomerFilter]
# Define the fields that you support to search
gender : String
gender_not : String
....
....
...
...
age : Int
age_gte : Int
}
type Query{
customers (filter:CustomerFilter) : [Customer]
}
And the query becomes :
{
customers (filter : {
OR: [
{ gender : 'MALE' } ,
{ age_gte: 20 }
]
}){
id
name
}
}
This is another filter model for reference. The idea is to tailor-made it such that it is just enough to handle all your application requirements without introducing any unnecessary filtering complexity.
Also , you most probably need to consider something like pagination if it potentially will return many data. It means you have to add an offset and limit to the input arguments for each query to somehow limit the number of record returned if you are doing offset-based pagination or take a look on Relay Specification if you want to do it in the cursor-based pagination style.
You need to define query, schema and resolver. Your Query will be like :
type Query {
nameItAsPerUsecase: Customer
}
Schema will be like these :
type Customer{
name : String
age: Int
.. add more fields as per as your need
}
Your resolver will be like this: -
#Component
public class CustomerQuery implements GraphQLQueryResolver {
#Autowired
private CustomerRepository customerRepository ;
public Customer getNameItAsPerUsecase() {
return customerRepository.findByGenderAndAgeGreaterThanEqual(String gender, int age);
}
}
Nested logic with the same and/or conjunction can be simplified into a single list.
For example, the following complex query:
or: [
{ or: [ { foo: { eq: "A" } }, { bar: { eq: "B" } } ] },
{ or: [ { baz: { eq: "C" } }, { quz: { eq: "D" } } ] }
]
} ) { ... }
Moreover, can be simplified into the following simplified query syntax:
queryPost(filter: {
or: [
{ foo: { eq: "A" } },
{ bar: { eq: "B" } },
{ baz: { eq: "C" } },
{ quz: { eq: "D" } }
]
} ) { ... }

Spring Repository Projection Get Child of One Object

I am using Spring Repository interfaces a lot. One was working fine for me but then I realized I needed a little bit more from it. I wanted to go one more level of get()
I work on an intranet so can't copy and paste but hopefully the following will give enough info to make it understandable...
#Entity
#Table(name="person")
class Person {
...
}
#Entity
#Table(name="requisite")
class Requisite {
...
#OneToOne
#JoinColumn
private Document document;
}
#Entity
#Table(name="person_requisite")
class PersonRequisite {
...
#ManyToOne
#JoinColumn(name="person_id")
private Person person;
...
#ManyToOne
#JoinColumn(name="requisite_id")
private Requisite requisite;
...
}
#Projection(name="personRequisiteProjection", types={PersonRequisite.class})
public interface PersonRequisiteProjection {
...
Person getPerson();
Requisite getRequisite();
...
}
Here is a representation of what i am getting now...
"personRequisites" : [ {
...
"requisite" : {
id : 1,
...
no document object or document id from the requisite
},
"person" : {
id : 33,
...
},
...
]
...
Here is a representation of what i want...
"personRequisites" : [ {
...
"requisite" : {
id : 1,
...
"document" : {
"id" : 55,
"name" : blah,
...
}
},
"person" : {
id : 33,
...
},
...
]
...
I know this is not correct but i basically want
#Projection(name="personRequisiteProjection", types={PersonRequisite.class})
public interface PersonRequisiteProjection {
...
//i know, this would be out of place if it worked but trying to emphasize what i want...
Document getRequisite().getDocument();
//i'd still want Requisite getRequisite() as well but you get what i am after
...
//or more appropriately, force document to show up in Requisite here...
Requisite getRequisite();
...
}
#Projection(name="personRequisiteProjection", types={PersonRequisite.class})
public interface PersonRequisiteProjection {
...
#Value("#{target.requisite.document}")
Document getRequisite().getDocument();
//i'd still want Requisite getRequisite() as well but you get what i am after
...
//or more appropriately, force document to show up in Requisite here...
Requisite getRequisite();
...
}

Resources