Spring data mongo aggregation mapping to object - mongodb

I want to map the results of an aggregation to an POJO without iterating through the raw results. The POJO is a field in the Collection on which I'm running the aggregation.
MatchOperation match = match(Criteria.where("drill").is(drill));
SortOperation sort = sort(DESC, "creationDate");
GroupOperation group = group("athlete").first("athlete").as("athlete");
LimitOperation limit = limit(10);
ProjectionOperation project = project("athlete");
Aggregation aggregation = newAggregation(match, sort, group, limit, project);
AggregationResults<Athlete> results = mongoTemplate.aggregate(aggregation, DrillResultInfo.class, Athlete.class);
List<Athlete> mappedResult = results.getMappedResults();
It returns the correct number of objects, but they have as the id the map of the object and the other properties are null.
The result:
id: { "_id" : { "$oid" : "57cd46780348276373579821"} , "_class" : "Athlete" , "firstName" : "Jenny" , "lastName" : "Smith" ....}
The rest of the properties are null.
The Collection:
public class DrillResultInfo {
#Id
private String id;
private Long resultId;
#DBRef
private Athlete athlete;
#DBRef
private Drill drill;
....
}
(.... represents left out data)
Update:
I've made some updates to the code to get it to work:
List<Athlete> respone = new ArrayList<>();
MatchOperation match = match(Criteria.where("drill").is(drill));
SortOperation sort = sort(DESC, "creationDate");
GroupOperation group = group("athlete");
LimitOperation limit = limit(5);
SkipOperation skip = skip(skipElements);
ProjectionOperation project = project("_id");
TypedAggregation<DrillResultInfo> agg = newAggregation(DrillResultInfo.class, match, sort, group, skip, limit, project);
AggregationResults<Object> results = mongoTemplate.aggregate(agg, Object.class);
List<Object> mappedResult = results.getMappedResults();
for (Object obj : mappedResult) {
Athlete ath = (Athlete) ((LinkedHashMap) obj).get("_id");
respone.add(ath);
}
return respone;
I would like to get rid of that for.

Related

How to make a Spring Data MongoDB GroupOperation on a single field but with an object as a result id?

Here is the group operation that I want to create depending on a nomenclature object.
private static GroupOperation createStatsGroupOperationFromNomenclature(Nomenclature nomenclature) {
Fields groupFields = Fields.fields("departmentCode");
nomenclature.getDepartmentCode().ifPresent(code -> groupFields.and("subDepartmentCode"));
nomenclature.getSubDepartmentCode().ifPresent(code -> groupFields.and("categoryCode"));
nomenclature.getCategoryCode().ifPresent(code -> groupFields.and("subCategoryCode"));
return group(groupFields)
.count().as("nbProducts")
.sum("$proposedMatchesAmount").as("nbProposedMatches")
.sum("$reviewedMatchesAmount").as("nbReviewedMatches");
}
With the previous function if I provide a departmentCode and a subDepartmentCode inside the nomenclature parameter, here is the mongo query that is executed :
{
_id: {
"departmentCode": "$departmentCode",
"subDepartmentCode": "$subDepartmentCode"
},
"nbProduct": {
$sum: 1
},
"proposedMatchesAmount": {
$sum: "$proposedMatchesAmount"
},
"reviewedMatchesAmount": {
$sum: "$reviewedMatchesAmount"
}
}
The result of this query are parsed in the following object :
#Builder
#Value
public class ProductsStatsDocument {
#Id
Nomenclature nomenclature;
Integer nbProducts;
Integer nbProposedMatches;
Integer nbReviewedMatches;
}
Problems append when I provide only a departmentCode inside the nomenclature parameter. Then the builded group operation has the following mongo query language equivalent:
{
_id: "$departmentCode",
"nbProduct": {
$sum: 1
},
"proposedMatchesAmount": {
$sum: "$proposedMatchesAmount"
},
"reviewedMatchesAmount": {
$sum: "$reviewedMatchesAmount"
}
}
And the result of this query couldn't be parsed to the previous ProductsStatsDocument because the result _id field id now a String and not a Nomenclature object.
Is it possible to force the group method to use an object as result _id field even with only one field ? Or is there an other way to build such a mongo group operation ?
=================================================================
Found the "why" of this issue. Here is a piece of code from spring data that transform the GroupOperation into a bson object :
} else if (this.idFields.exposesSingleNonSyntheticFieldOnly()) {
FieldReference reference = context.getReference((Field)this.idFields.iterator().next());
operationObject.put("_id", reference.toString());
} else {
And here is the exposesSingleNonSyntheticFieldOnly method :
boolean exposesSingleNonSyntheticFieldOnly() {
return this.originalFields.size() == 1;
}
As you can see, as soon as there is only one field to group on, it's used as _id result value.
So finally the solution that seems to works for now is to create a custom AggregationOperation that manage the document transformation _id part :
public class ProductsStatsGroupOperation implements AggregationOperation {
private static GroupOperation getBaseGroupOperation() {
return group()
.count().as("nbProducts")
.sum("$proposedMatchesAmount").as("nbProposedMatches")
.sum("$reviewedMatchesAmount").as("nbReviewedMatches");
}
private final Nomenclature nomenclature;
public ProductsStatsGroupOperation(Nomenclature nomenclature) {
this.nomenclature = nomenclature;
}
#Override
public Document toDocument(AggregationOperationContext context) {
Document groupOperation = getBaseGroupOperation().toDocument(context);
Document operationId = new Document();
for (Field field : getFieldsToGroupOn()) {
FieldReference reference = context.getReference(field);
operationId.put(field.getName(), reference.toString());
}
((Document)groupOperation.get("$group")).put("_id", operationId);
return groupOperation;
}
private Fields getFieldsToGroupOn() {
Fields groupFields = Fields.fields("departmentCode");
if (nomenclature.getDepartmentCode().isPresent()) {
groupFields = groupFields.and("subDepartmentCode");
}
if (nomenclature.getSubDepartmentCode().isPresent()) {
groupFields = groupFields.and("categoryCode");
}
if (nomenclature.getCategoryCode().isPresent()) {
groupFields = groupFields.and("subCategoryCode");
}
return groupFields;
}
}
There is a bad thing about this solution: the overrided method toDocument seems to be deprecated.

MongoDB - how do I add a field to a nested object in one collection using a field from another collection?

I have two collections A and B.
Collection A has a nested object 'nested', which has a field 'id'.
Collection B also has a field 'id' and another 'type'.
My question is:
How can I add the 'type' field of collection B to collection A's nested object, where the id's match?
Originally, I was looking for a magic mongo one-liner query to do the job, but I think I'll tap out on that idea!
Finally, I used the following Java solution:
private void updateNestedInCollectionAWithTypeFromCollectionBWhereIdsMatch(List<CollectionBPojo> collectionB, MongoCollection<Document> collectionA) {
StreamUtils.createStreamFromIterator(collectionA.find().iterator()).forEach(d -> {
final List<Document> nestedList = (List<Document>)d.get("nested");
for (int i = 0; i < nestedList.size(); i++) {
final Document nested = nestedList.get(i);
if(!nested.containsKey("type")) {
final String id = nested.getString("id");
final Optional<CollectionBPojo> collectionBPojo = collectionB.stream().filter(g -> g.getId().equals(id)).findFirst();
if (collectionBPojo.isPresent()) {
final String type = collectionBPojo.get().getType();
final Document query = new Document("id", d.get("id"));
final Document update = new Document();
update.append("$set", new BasicDBObject("nested" + "." + i + ".type", type));
updates.add(new UpdateOneModel<>(query, update));
}
}
}
});
if(!updates.isEmpty()) {
final BulkWriteResult result = pipelineRunsCollection.bulkWrite(updates);
} else {
System.out.print.ln("Nothing to update on db: " + db.getName());
}
}
}

How to do OR inside of AND?

SQL query=>
where and ((subtype="dailyMessage" and registDate="today") or(subtype !="dailyMessage"))
i want to retrive this query by using mongodb.
i have this collection. (today is '2017-08-16T15:48:19.947Z')
{
"_id" : ObjectId("597768443b1fd6308c0350c0"),
"type" : "message",
"subtype" : "dailyMessage",
"message" : test1",
"registDate" : ISODate("2017-08-16T15:48:19.947Z")
},
{
"_id" : ObjectId("597768443b1fd6308c0350c1"),
"type" : "message",
"subtype" : "dailyPush",
"message" : test2",
"registDate" : ISODate("2017-07-25T15:48:19.947Z")
},
{
"_id" : ObjectId("597768443b1fd6308c0350c2"),
"type" : "message",
"subtype" : "dailyPush",
"message" : test3",
"registDate" : ISODate("2017-07-24T15:48:19.947Z")
}
here is my code(spring boot mongodb , using java 8)
List<String> listOfmessage = new ArrayList<String>();
listOfSubtype.add("dailyMessage");
criteria = Criteria.where("test").is("james");
criteria.andOperator(Criteria.where("registDate").gte(LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT)).lte(LocalDateTime.now()) , Criteria.where("subtype").in(listOfmessage) .orOperator(Criteria.where("subtype").nin(listOfmessage)));
my problem is that how can i insert orOperator on the andOperator?
As mentioned in the comment, the and outside the whole condition doesn't make sense. The below code is written for the following condition.
(condition1 AND condition2) OR condition3
Code:-
You may need to alter the below code to get the MongoOperations object based on your Spring configuration.
"register" is the collection name. You can change it accordingly as per your collection name.
public Boolean getRegisterData() {
MongoOperations mongoOperations = getMongoConnection();
List<String> listOfSubType = new ArrayList<String>();
listOfSubType.add("dailyMessage");
List<String> listOfMessage = new ArrayList<String>();
listOfMessage.add("test1");
Criteria criteriaSubTypeAndDate = new Criteria();
criteriaSubTypeAndDate.andOperator(Criteria.where("registDate")
.gte(LocalDateTime.of(LocalDate.now(), LocalTime.MIDNIGHT)).lte(LocalDateTime.now()),
Criteria.where("subtype").in(listOfSubType));
Criteria criteriaSubType = Criteria.where("subtype").ne("dailyMessage");
Criteria criteriaFull = new Criteria();
criteriaFull.orOperator(criteriaSubTypeAndDate, criteriaSubType);
Query query = new Query();
query.addCriteria(criteriaFull);
System.out.println(query);
mongoOperations.executeQuery(query, "register", new RegistryDocumentCallbackHandler());
return true;
}
Result set document processing class:-
processDocument() method will get executed for each document in the result set.
public class RegistryDocumentCallbackHandler implements DocumentCallbackHandler {
#Override
public void processDocument(DBObject dbObject) throws MongoException, DataAccessException {
System.out.println("Registry collections data ===>" + dbObject.toString());
}
}

Specify multiple criteria's in spring mongo db query

I am iterating over a list of key/value pairs and executing find for each key/value. Can I create a single query document to be kind of union in sql, So that there will be only one database call.
List<User> userList = new ArrayList<User>();
for (Map accounts:attributes) {
Query query = new Query();
List<Criteria> andCriteriaList = new ArrayList<Criteria>();
accounts.forEach((key, value) -> {
Criteria criteria = Criteria.where((String) key).is(value);
andCriteriaList.add(criteria);
});
query.addCriteria(new Criteria().andOperator(andCriteriaList.toArray(new Criteria[andCriteriaList.size()])));
if (mongoTemplate.exists(query, User.class)) {
userList.add((User)mongoTemplate.find(query, User.class));
//System.out.println(mongoTemplate.find(query, User.class));
}
Thanks,
You can refactor your code to create $or expressions. No explicit $and operator needed.
Something like
Query orQuery = new Query();
Criteria orCriteria = new Criteria();
List<Criteria> orExpression = new ArrayList<>();
for (Map<String, Object> accounts : attributes) {
Criteria expression = new Criteria();
accounts.forEach((key, value) -> expression.and(key).is(value));
orExpression.add(expression);
}
orQuery.addCriteria(orCriteria.orOperator(orExpression.toArray(new Criteria[orExpression.size()])));
List<User> userList = mongoOperations.find(orQuery, User.class);
This should output query like
{ "$or" : [{ "key1" : "value1", "key2" : "value2" }, { "key3" : "value3", "key4" : "value4" }] }
if you want to query multiple fields(field1, field2, ....) with values below is the solution
Query query = new Query();
List<Criteria> criteria = new ArrayList<>();
criteria.add(Criteria.where(field1).is(field1val));
criteria.add(Criteria.where(field2).is(field2val));
// you can add all your fields here as above
query.addCriteria(new Criteria().andOperator(criteria.toArray(new Criteria[criteria.size()])));
List<JSONObject> filteredVals = mongoOperations.find(query, JSONObject.class);
the above return filteredVals is JSONObject

how to calculate average of a particular field in mongodb spring?

how to calculate average of a field in mongoDB and spring. we have $avg() function for terminal use but how to execute it with mongotemplate.
for example in
db.sales.aggregate(
[
{
$group:
{
_id: "$item",
avgAmount: { $avg: { $multiply: [ "$price", "$quantity" ] } },
avgQuantity: { $avg: "$quantity" }
}
}
]
)
we are calculating average here so how can we execute it with mongotemplate.
Now I am using a function to get average rating
i am using function like this..
public List getrating() {
TypedAggregation<RatingReviewModel> agg = newAggregation(RatingReviewModel.class,
group("hospitalid")
.avg("rating").as("avgrating")
);
AggregationResults<DBObject> result = operations.aggregate(agg, DBObject.class);
List<DBObject> resultList = result.getMappedResults();
return resultList;
}
but at the time of debugging resultList is Empty so it is returning nothing.
Suppose your Sale object is defined as:
class Sale {
String id;
String item;
double price;
int quantity;
}
Using the mongotemplate you would need a $project stage in the pipeline before hand to get the calculated fields, which can be a bit counter-intuitive because with the native MongoDB aggregation all is done in one $group operation pipeline rather than splitting the aggregation into two stages, thus:
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
TypedAggregation<Sale> agg = newAggregation(Sale.class,
project("quantity")
.andExpression("price * quantity").as("totalAmount"),
group("item")
.avg("totalAmount").as("avgAmount")
.avg("quantity").as("avgQuantity")
);
AggregationResults<DBObject> result = mongoTemplate.aggregate(agg, DBObject.class);
List<DBObject> resultList = result.getMappedResults();
The above can also be achieved using the native Java Driver implementation:
ApplicationContext context = new AnnotationConfigApplicationContext(SpringMongoConfig.class);
MongoOperations operation = (MongoOperations) context.getBean("mongoTemplate");
BasicDBList pipeline = new BasicDBList();
String[] multiplier = { "$price", "$quantity" };
pipeline.add(
new BasicDBObject("$group",
new BasicDBObject("_id", "$item")
.append("avgAmount", new BasicDBObject(
"$avg", new BasicDBObject(
"$multiply", multiplier
)
))
.append("avgQuantity", new BasicDBObject("$avg", "$quantity"))
)
);
BasicDBObject aggregation = new BasicDBObject("aggregate", "sales")
.append("pipeline", pipeline);
System.out.println(aggregation);
CommandResult commandResult = operation.executeCommand(aggregation);