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"}
)
));
Related
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
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!
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");
How do I go about writing an $addFields query in Spring Data MongoDB Reactive for a simpler and a slightly more complex field addition as shown below:
db.getCollection("mycollection").aggregate(
[
{
"$addFields" : {
"existingObjectField.newFieldArray" : [
"$existingObjectField.existingFieldObject"
]
}
},
{
"$addFields" : {
"existingFieldArray" : {
"$map" : {
"input" : "$existingFieldArray",
"as" : "item",
"in" : {
"existingFieldObject" : {
"_id" : "$$item. existingFieldObject._id",
"newFieldArray" : [
"$$item. existingFieldObject.existingFieldObject"
]
}
}
}
}
}
},
{
"$out" : "mycollection"
}
]
);
In the first add fields, I am simply creating a new array field with one of the existing object field.
In the 2nd add fields, doing the same but within an object in an array in the document.
Like for match/unwind AddFieldOperation is not present in spring data mongo But you can write your own and also a custom Aggregation class to add caller method to addFieldOpration as below.
public class AddFieldOperation implements AggregationOperation {
private final Document document;
/**
* Creates a new {#link MatchOperation} for the given {#link CriteriaDefinition}.
*
* #param criteriaDefinition must not be {#literal null}.
*/
public AddFieldOperation(final Document document) {
Assert.notNull(document, "Criteria must not be null!");
this.document = document;
}
/*
* (non-Javadoc)
*
* #see org.springframework.data.mongodb.core.aggregation.AggregationOperation#toDocument(org.
* springframework.data.mongodb.core.aggregation.AggregationOperationContext)
*/
#Override
public Document toDocument(final AggregationOperationContext context) {
return new Document("$addFields", this.document);
}
}
Now make CustomAggregation class.
public class CustomAggregation extends Aggregation {
public static AddFieldOperation addField(final Document document) {
return new AddFieldOperation(document);
}
}
Everything is ready you need to call Addfield method and pass all query in Document object example:-
AddFieldOperation addField =
CustomAggregation.addField(new Document().append("fieldName", fieldValue));
Note
Document class is from import package org.bson.Document;
Which is a representation of a document as a {#code Map}.
All aggregation operation implemented in spring data mongo is finally converted to the Document object and this will execute in the shell. So if some of the aggregation pipelines are not yet implemented in spirng data, in that case, we can write our own and pass the query which is written in mongo shell we can just pass it in the Document object.
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();
}