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

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);

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.

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

Spring data mongo aggregation mapping to object

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.

MongoDB C# Driver Update Collection with Concatenated string

How do I convert this SQL to MongoDB query using C# Driver
UPDATE dbo.MyTable SET ConcatField = CONCAT(Field1, Field2, Field3, Field4, Field5)
WHERE Id = 21
Using MongoDB.Driver 2.2.3.3
I need MongoDB query using BsonDocument type, I don't have Strong types for my Mongo Collections as Collection is not based on fixed schema.
Trying something like this
var items = myCollection.FindSync(filter).ToList();
foreach (var item in items)
{
UpdateDefinition<BsonDocument> updateDefinition =
new BsonDocumentUpdateDefinition<BsonDocument>(item.Merge(ListOfStrinForSelectedFields.ToBsonDocument()));
myCollection.UpdateManyAsync(filter, updateDefinition);
}
This will be my Shell Script
var cursor = db.MyCollection.find({ "Id": 21 }), // Or what ever your find conditions is
bulkUpdateOps = [];
cursor.forEach(function(doc){
var ConcatField = doc.Field1 + doc.Field2 + doc.Field3 ;
bulkUpdateOps.push({
"updateOne": {
"filter": { "_id": doc._id },
"update": { "$set": { "MyConCatField": ConcatField } }
}
});
if (bulkUpdateOps.length == 1000) {
db.MyCollection.bulkWrite(bulkUpdateOps);
bulkUpdateOps = [];
}
});
if (bulkUpdateOps.length > 0) { db.MyCollection.bulkWrite(bulkUpdateOps); }
then execute it in c# with RunCommandAsync method from MongoDatabase.
var result = await mongoDatabase.RunCommandAsync<BsonDocument>(BsonDocument.Parse(command));
Note: you will have to modify the command string using pipelines and parse it to BsonDocument.

How to update a inner/embedded document in a mongodb using mongotemplate

Can some one help me to write a code to update "coordinates". I was able to update the address but not the coordinates.
{
"_id": "2c9080e54b4ee7ac014b4ee8e5100000",
"_class": "com.myparking.dataservice.mongodb.documents.ParkingSiteDocument",
"address": {
"streetAddress": "bellandur",
"locality": "ORR",
"region": "bangalore",
"country": "india",
"postalCode": "560102"
},
"geoLocation": {
"coordinates": [ 12.934292, 77.680215 ],
"type": "Point"
}
}
My code goes like this: When I update the address it is working but I am not able to update the coordinates.
public ParkingSiteDocument updateParkingSite(final ParkingSpaceDTO pSpace) {
ParkingSiteDocument parkingSpace = null;
try{
// If the collection doesn't exist return.
if (!mongoTemplate.collectionExists(ParkingSiteDocument.class)) {
// return.
return null;
}
Query query = new Query();
// query to fetch the parking site based on the id.
query.addCriteria(Criteria.where("pSiteId").is(pSpace.getpSpaceId()));
parkingSpace = mongoTemplate.findOne(query, ParkingSiteDocument.class);
// If the parking space is not available return;
if(parkingSpace == null) {
return null;
}
// Update address and coordinates
Update update = new Update();
// Updating the address.
if(pSpace.getAddress() != null) {
Address newAddress = new Address();
newAddress.setCountry(pSpace.getAddress().getCountry());
newAddress.setLocality(pSpace.getAddress().getLocality());
newAddress.setPostalCode(pSpace.getAddress().getPostalCode());
newAddress.setRegion(pSpace.getAddress().getRegion());
newAddress.setStreetAddress(pSpace.getAddress().getStreetAddress());
// converting it into mongo document.
MongoConverter converter = mongoTemplate.getConverter();
DBObject newRec = (DBObject) converter.convertToMongoType(newAddress);
update.set("address", newRec);
}
// Update the geolocation coordinates
if(pSpace.getGeoCoordinates() != null) {
// creating new coordinates from the input DTO.
Double[] coordinates = new Double[]{pSpace.getGeoCoordinates().getLongitude(),
pSpace.getGeoCoordinates().getLatitude()};
MongoConverter converter = mongoTemplate.getConverter();
DBObject newRec = (DBObject) converter.convertToMongoType(coordinates);
update.set("geoLocation.coordinates", newRec);
}
// update query.
mongoTemplate.updateFirst(query, update, ParkingSiteDocument.class);
} catch(Exception e) {
logger.error(this.getClass().getSimpleName(), "updateParkingSite | Exception" + e.getMessage());
}
return parkingSpace;
}
if (!mongoTemplate.collectionExists(ParkingSiteDocument.class))//or document name
mongoTemplate.createCollection(ParkingSiteDocument.class);//or document name
DBCollection db=mongoTemplate.getCollection(ParkingSiteDocument.class);//document name
BasicDBObject updateDocument = new BasicDBObject();
DBObject update = new BasicDBObject("$set",
new BasicDBObject("geoLocation",
new BasicDBObject("coordinates", "12.934292,77.680215")));
BasicDBObject searchQuery= new BasicDBObject().append("_id", new ObjectId("54d1d939e4b044860afcdf6d"));
db.update(searchQuery, update);