Spring data mongo - Get sum of array of object in mongo - mongodb

I have an issue getting the sum of an array of objects, Document is mentioned below.
{
"orderNumber":123,
"items":[
{
"price":2
},
{
"price":10
}
]
}
I need the below document
{
"orderNumber":123,
"totalItemsValue":12,
"items":[
{
"price":2
},
{
"price":10
}
]
}
Sum of prices in totalItemsValue field.I need a solution in spring mongo data.I will be thankful.

There ae lot of ways, easiest way is given below. Spring data doesn't provide any operation for $addFields. So we do a Trick.
Autowire the mongoTemplate
#Autowired
private MongoTemplate mongoTemplate;
And the method is like
public List<Object> test() {
Aggregation aggregation = Aggregation.newAggregation(
a-> new Document("$addFields",
new Document("totalItemsValue",
new Document("$reduce"
new Document()
.append("input","$items")
.append("initialValue",0)
.append("in",
new Document("$add",Arrays.asList("$$value",1))
)
)
)
)
).withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(YOUR_COLLECTION.class), Object.class).getMappedResults();
}
Working Mongo playground

Related

How to achieve MongoDB aggregate query pipeline stage { $group: {_id: "$$ROOT"} } from springboot?

In my spring-boot project, MongoDB aggregation queries are tailored using different interfaces in org.springframework.data.mongodb.core.aggregation package like AggregationOperation, GroupOperation etc. as follows.
List<AggregationOperation> aggList = new ArrayList<>();
MatchOperation match = match(//pass filters here...
aggList.add(match);
GroupOperation group = new GroupOperation(...//group fields and aliases
aggList.add(group);
ProjectionOperation projection= project().andExclude("_id");
//append other projections here
...
aggList.add(projection);
Aggregation aggregationOp = Aggregation.newAggregation(aggList);
AggregationResults<Object> aggResults = this.getOperations().aggregate(aggregationOp , collectionName,
Object.class);
return aggResults.getMappedResults();
This is working well so far.
But now I need to add a group with $$ROOT stage (In order to eliminate duplicates) like below.
{ $group: { _id: "$$ROOT" } }
I have tried different approaches but failed.
The failed approaches are given below.
//Approach 1:
aggList.add(Aggregation.group(Aggregation.ROOT));
which gives me below error,
java.lang.IllegalArgumentException: Invalid reference '$$ROOT'!
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:100) ~[spring-data-mongodb-2.1.2.RELEASE.jar:2.1.2.RELEASE]
and approach 2,
//Approach 2:
aggList.add(new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$group",
new Document("_id", Aggregation.ROOT));
}
});
which gave me below error,
java.lang.IllegalArgumentException: Invalid reference '_id'!
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:100) ~[spring-data-mongodb-2.1.2.RELEASE.jar:2.1.2.RELEASE]
Can anyone help me out?
ANSWER:
Below code works!
aggList.add(new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$group",
new Document("_id", "$$ROOT"));
}
});
Thank you Varman for help!

How to call MongoDB addFields in Spring Mongo?

I have the following type of object in my MongoDB
{
...,
"name": {
"en": "...",
"am": "...",
"ru": "..."
},
...
}
Now I want to run the following query using Spring Data Mongo
db.categories.aggregate({$addFields: {'name': '$name.am'}})
where the am part in $name.am is the locale of the name, hence it must be dynamic. I looked through the MongoTemplate API, and as far as I found, there is no support of addFields operation. Is there any way to achieve this?
You need to implement AggregationOperation
Single use
AggregationOperation addFields = new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$addFields", new Document("name", "$name.am"));
}
};
Generic
public AggregationOperation aggregateAddFields(final String field, final String valueExpresion) {
AggregationOperation addFields = new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$addFields", new Document(field, valueExpresion));
}
};
return addFields;
}
...
AggregationOperation addFields = aggregateAddFields("name", "$name.am");

Spring mongo slice array in embedded document

The slice method in aggreation for array within an embedded document is not working for me using spring mongo template.
Example:
Invoice collection:
{
"reference_number": "aaa111",
"historical_data": {
"year": 2010,
"data": [
{
"item_name": "Apple",
"price": 50
},
{
"item_name": "Book",
"price": 200
}
]
}
}
Using mongoTemplate I would like to get only the historical data in slices.
For the arrays which needs to be sliced that appear directly under the root I had found a solution using aggregation
Refer : Spring mongo repository slice
Applying a similar query for array in an embedded document returns an empty list even if there is data.
The query that I was trying was :
TypedAggregation<Invoice> agg = newAggregation(Invoice.class,
match(where("reference_number").is(referenceNumber)),
project.andExpression("historicalData.data").slice(limit, offset));
AggregationResults<Invoice> result = mongoTemplate.aggregate(agg, Invoice.class, Invoice.class);
But this returns an empty list of data.
Is there any other alternative way to get the sliced result for arrays within an embedded document?
Invoice.java
#Data
#Document(collection = "invoice")
public class Invoice {
#Id
private String id;
#NotEmpty
#Indexed(unique = true)
#Field("reference_number")
private String referenceNumber = UUID.randomUUID().toString();
#Valid
#Field("historical_data")
private HistoricalData historicalData = new HistoricalData();
}
HistoricalData:
#Data
public class HistoricalData {
#NotEmpty
#Field("year")
private Intger year;
#Valid
#NotNull
#Field("data")
private List<InvoiceData> data = new LinkedList<>();
}
Note : I have also tried :
TypedAggregation<Invoice> agg = newAggregation(Invoice.class,
match(where("reference_number").is(referenceNumber)),
project.andExpression("historical_data.data").slice(limit, offset));
AggregationResults<Invoice> result = mongoTemplate.aggregate(agg, Invoice.class, Invoice.class);
But this gave me a PropertyPathException.
Thanks in advance!!
After a weeks struggle I have figured out a solution for this:
ProjectionOperation project = project().and("historicalRevisionData.data").slice(limit, offset).as("historical_revision_data.data")
.andInclude("id")
.and("referenceNumber").as("reference_number");
TypedAggregation<Invoice> agg = newAggregation(Invoice.class,
match(where("reference_number").is(referenceNumber)),
project);
AggregationResults<TaxInvoice> aggregate = mongoTemplate.aggregate(agg, Invoice.class, Invoice.class);
Hoping that this would help someone else too.

return the last embeded data in a collection

In a spring-boot data mongodb application I whould like to return the last element of an embeded collection.
My document is :
#Document
public class ConnectedObject {
#Id
private String uuid;
private List<Measure> measures = new ArrayList<>();
}
public class Measure {
private LocalDateTime timestamp;
private long stepsCount;
}
Exemple of data in mongoDb:
{
"_id":"aaaa",
"measures":[
{"timestamp":"2018-04-05T08:20:33.561Z","stepsCount":"0"},
{"timestamp":"2018-04-05T08:21:35.561Z","stepsCount":"10"},
{"timestamp":"2018-04-05T08:20:35.561Z","stepsCount":"0"}
]
}
I whould like to get the last measure (filter by timestamp field) of the connectedObject (filter onthe uuid).
I don't know how to write the query using MongoTemplate.
I already have custom repository in the project.
Something like the query below should give what you need
db.collection.aggregate([
{$match: {'$uuid':'12345'}},
{$sort:{'$measure.timestamp':1}},
{$project:{
uuid: 1,
last: { $arrayElemAt: [ "$measures", -1 ] }
}
}
])
After a lot of try, the one running is :
public Measure getLastMeasureOf(String uuid) {
final Aggregation agg = newAggregation(
match(Criteria.where("uuid").is(uuid)),
unwind("measures"),
sort(Sort.Direction.DESC, "measures.timestamp"),
group("uuid").first("$$ROOT").as("result"),
project("result.uuid").and("result.measures").as("last")
);
final AggregationResults<ObjectWithLastMeasure> results
= template.aggregate(agg, ConnectedObject.class, ObjectWithLastMeasure.class);
final ObjectWithLastMeasure owlm = results.getUniqueMappedResult();
return owlm == null ? null : owlm.getLast();
}

How do I Project an Array Field with Spring Data

I am trying to create an aggregation pipeline using Spring Data MongoDB that projects a new array field into the pipeline. How can I accomplish this using Spring Data?
The Pipeline stage I am trying to replicate is as follows:
{
$project: {
"aceId": 1,
"startActivityDateTime": 1,
"lastActivityDateTime": 1,
"eventInfo": [
"$applicationInfo",
"$riskAssessmentInfo",
"$policyInfo",
"$submissionInfo"
]
}
},
I had the same Problem and found a Solution. You have to create a CustomAggregationOperation class as in this answer suggested:
https://stackoverflow.com/a/29186539/5033846
public class CustomProjectAggregationOperation implements AggregationOperation {
private DBObject operation;
public CustomProjectAggregationOperation (DBObject operation) {
this.operation = operation;
}
#Override
public DBObject toDBObject(AggregationOperationContext context) {
return context.getMappedObject(operation);
}
}
Then you can achieve your projection stage as follows:
new CustomAggregationOperation(new BasicDBObject("$project",
new BasicDBObject("aceId", 1)
.appending("startActivityDateTime", 1)
.appending("lastActivityDateTime", 1)
.appending("eventInfo",
new Object[]{
"$applicationInfo",
"$riskAssessmentInfo",
"$policyInfo",
"$submissionInfo"}
)
));