using mongodb shell, I am able to perform an aggregation query that retrieves the whole document.
In order to do that I use the $$ROOT variable.
db.reservations.aggregate([
{ $match : { hotelCode : "0360" } },
{ $sort : { confirmationNumber : -1 , timestamp: -1 } },
{ $group : {
_id : "$confirmationNumber",
timestamp :{$first : "$timestamp"},
fullDocument :{$first : "$$ROOT"}
}}
])
It retrieves objects whose content is confirmationNumber, timestamp, fullDocument.
fullDocument is the whole document.
I am wondering if it is possible to do the same with Spring-Data and the aggregation framework.
My java code is:
TypedAggregation<ReservationImage> aggregation = newAggregation(
ReservationImage.class,
match(where("hotelCode").is(hotelCode)),
sort(Direction.DESC,"confirmationNumber","timestamp"),
group("confirmationNumber").
first("timestamp").as("timestamp").
first("$$ROOT").as("reservationImage"));
List<myClass> items = mongoTemplate.aggregate(
aggregation,
myClass.class).getMappedResults();
the error is :
org.springframework.data.mapping.PropertyReferenceException: No property $$ found for type myClass
Do you have any ideas?
Thanks.
We created https://jira.spring.io/browse/DATAMONGO-954 to track the support for accessing System Variables from MongoDB Pipeline expressions.
Once that is in place, you should be able to write:
Aggregation agg = newAggregation( //
match(where("hotelCode").is("0360")), //
sort(Direction.DESC, "confirmationNumber", "timestamp"), //
group("confirmationNumber") //
.first("timestamp").as("timestamp") //
.first(Aggregation.ROOT).as("reservationImage") //
);
I've seen this sort of thing before and it is not just limited to variable names such as $$ROOT. Spring data has it's own ideas about how to map "properties" of the document in the pipeline. Another common problem is simply projecting a new or calculated field that essentially has a new "property" name to it that does not get recognized.
Probably the best approach is to "step down" from using the helper classes and methods and construct the pipeline as BSON documents. You can even get the underlying collection object and the raw output as a BSON document, yet still cast to your typed List at the end.
Mileage may vary to your actual approach,but essentially:
DBObject match = new BasicDBObject(
"$match", new BasicDBObject(
"hotelCode", "0360"
)
);
DBObject sort = new BasicDBObject(
"$sort", new BasicDBObject(
"cofirmationNumber", -1
).append("timestamp", -1)
);
DBObject group = new BasicDBObject(
"$group", new BasicDBObject(
"_id", "confirmationNumber"
).append(
"timestamp", new BasicDBObject(
"$first", "$timestamp"
)
).append(
"reservationImage", new BasicDBObject(
"$first", "$$ROOT"
)
)
);
List<DBObject> pipeline = Arrays.asList(match,sort,group);
DBCollection collection = mongoOperation.getCollection("collection");
DBObject rawoutput = (DBObject)collection.aggregate(pipeline);
List<myClass> items = new AggregationResults(List<myClass>, rawoutput).getMappedResults();
The main thing is moving away from the helpers that are getting in the way and constructing the pipeline as it should be free of the imposed restrictions.
Related
I'm rather new to MongoDB and I'm trying to create a query which I though would be pretty trivial (well, alteast with SQL it would) but I can't get it done.
So have a collection patients in this collections a single patient is identified using the id property. (NOT mongodbs _id!!) There can be multiple version of a single patient, his version is determined by the meta.versionId field.
In order to query for all "current versions of patients" I need to get for every patient with a specific id the one with the maximum versionId.
So far I've got this:
AggregateIterable<Document> allPatients = db.getCollection("patients").aggregate(Arrays.asList(
new Document("$group", new Document("_id", "$id")
.append("max", new Document("$max", "$meta.versionId")))));
allPatients.forEach(new Block<Document>() {
#Override
public void apply(final Document document) {
System.out.println(document.toJson());
}
});
Which results in the following output (using my very limited test data):
{ "_id" : "2.25.260185450267055504591276882440338245053", "max" : "5" }
{ "_id" : "2.25.260185450267055504591276882441338245099", "max" : "0" }
Seems to work so far, but I need to get the whole patients collection.
Now I only know that for the id : 2.25.260185450267055504591276882440338245053 the max version is "5" and so on. Of course I could now create an own query for every single entry and sequentially get each patient document for a specific id/versionId-combo from mongodb but this seems like a terrible solution! Is there any other way to get it done?
If you know the columns that you want to retrieve , say patient name , address, etc I guess you can append those columns to the document with value 1.
AggregateIterable<Document> allPatients = db.getCollection("patients").aggregate(Arrays.asList(
new Document("$group", new Document("_id", "$id")
.append("max", new Document("$max", "$meta.versionId")).append("name",1).append("address",1))));
An approach that could work for you would be to first order the documents getting in the pipeline by the meta.versionId field using the $sort pipeline operator. However, be aware that the $sort stage has a limit of 100 megabytes of RAM. By default, if it exceeds this limit, $sort will produce an error.
To allow for the handling of large datasets, set the allowDiskUse option to true to enable $sort operations to write to temporary files. See the allowDiskUse option in aggregate() method for details.
After sorting, you can then group the ordered documents, carry out the aggregation using the $first or $last operators (depending on the previous sort direction) to get the other fields.
Consider running the following mongo shell pipeline operation as a way of
demonstrating this concept:
Mongo shell
pipeline = [
{ "$sort": {"meta.versionId": -1}}, // order the documents by the versionId field descending
{
"$group": {
"_id": "$id",
"max": { "$first": "$meta.versionId" }, // get the maximum versionId
"active": { "$first": "$active" }, // Whether this patient's record is in active use
"name": { "$first": "$name" }, // A name associated with the patient
"telecom": { "$first": "$telecom" }, // A contact detail for the individual
"gender": { "$first": "$gender" }, // male | female | other | unknown
"birthDate": { "$first": "$birthDate" } // The date of birth for the individual
/*
And many other fields
*/
}
}
]
db.patients.aggregate(pipeline);
Java test implementation
public class JavaAggregation {
public static void main(String args[]) throws UnknownHostException {
MongoClient mongo = new MongoClient();
DB db = mongo.getDB("test");
DBCollection coll = db.getCollection("patients");
// create the pipeline operations, first with the $sort
DBObject sort = new BasicDBObject("$sort",
new BasicDBObject("meta.versionId", -1)
);
// build the $group operations
DBObject groupFields = new BasicDBObject( "_id", "$id");
groupFields.put("max", new BasicDBObject( "$first", "$meta.versionId"));
groupFields.put("active", new BasicDBObject( "$first", "$active"));
groupFields.put("name", new BasicDBObject( "$first", "$name"));
groupFields.put("telecom", new BasicDBObject( "$first", "$telecom"));
groupFields.put("gender", new BasicDBObject( "$first", "$gender"));
groupFields.put("birthDate", new BasicDBObject( "$first", "$birthDate"));
// append any other necessary fields
DBObject group = new BasicDBObject("$group", groupFields);
List<DBObject> pipeline = Arrays.asList(sort, group);
AggregationOutput output = coll.aggregate(pipeline);
for (DBObject result : output.results()) {
System.out.println(result);
}
}
}
I am querying the mongodb using mongodb-java-driver aggregation api. And I find when query the db in a aggregation way using com.mongodb.DBCollection.aggregate a cursor interface will be returned.
Here is the method signature:
com.mongodb.DBCollection
public com.mongodb.Cursor aggregate(java.util.List<com.mongodb.DBObject> pipeline,
com.mongodb.AggregationOptions options)
But this returned cursor is not skip-able, ie, it doesn't have a skip method as the DBCursor class does. And the official document don't give a hint of doing that.
Does that mean when I do a aggregate query, and when I want to do a pagenation ,I can only retrieve the whole result set and skip the un-needed items myself?
You could of course skip them yourself programmatically, but there is a way to do it:
Aggregator skip
But you must skip with the aggregate, you can not skip afterwards (Then you would either need to aggregate again or skip programmatically)
{ $skip: <positive integer> }
You should use $skip in mongo aggregation also using java. Let's check following two links
mongo java aggregation
and
mongo $skip in aggregation
Using above two links you able to set your aggregation query as below
// unwind if required
DBObject unwind = new BasicDBObject("$unwind", "$your unwind field");
// create pipeline operations, with the $match
DBObject match = new BasicDBObject("$match", new BasicDBObject("given key", "matching value"));
// Now the $group operation if required
DBObject groupFields = new BasicDBObject("_id", "$group field");
groupFields.put("count", new BasicDBObject("$sum", 1));
DBObject group = new BasicDBObject("$group", groupFields);
// build the $projection operation
DBObject fields = new BasicDBObject("_id", 0);
fields.put("count", "$count");
DBObject project = new BasicDBObject("$project", fields);
// skip
DBObject skip = new BasicDBObject("$skip", positive integer);
// run aggregation
List < DBObject > pipeline = Arrays.asList(match, group, project, skip);
AggregationOutput output = collectionName.aggregate(pipeline);
for (DBObject result: output.results()) {
System.out.println(result);
}
I am using Spring Data MongoDB and would like to perform a Bulk Update just like the one described here: http://docs.mongodb.org/manual/reference/method/Bulk.find.update/#Bulk.find.update
When using regular driver it looks like this:
The following example initializes a Bulk() operations builder for the items collection, and adds various multi update operations to the list of operations.
var bulk = db.items.initializeUnorderedBulkOp();
bulk.find( { status: "D" } ).update( { $set: { status: "I", points: "0" } } );
bulk.find( { item: null } ).update( { $set: { item: "TBD" } } );
bulk.execute()
Is there any way to achieve similar result with Spring Data MongoDB ?
Bulk updates are supported from spring-data-mongodb 1.9.0.RELEASE. Here is a sample:
BulkOperations ops = template.bulkOps(BulkMode.UNORDERED, Match.class);
for (User user : users) {
Update update = new Update();
...
ops.updateOne(query(where("id").is(user.getId())), update);
}
ops.execute();
You can use this as long as the driver is current and the server you are talking to is at least MongoDB, which is required for bulk operations. Don't believe there is anything directly in spring data right now (and much the same for other higher level driver abstractions), but you can of course access the native driver collection object that implements the access to the Bulk API:
DBCollection collection = mongoOperation.getCollection("collection");
BulkWriteOperation bulk = collection.initializeOrderedBulkOperation();
bulk.find(new BasicDBObject("status","D"))
.update(new BasicDBObject(
new BasicDBObject(
"$set",new BasicDBObject(
"status", "I"
).append(
"points", 0
)
)
));
bulk.find(new BasicDBObject("item",null))
.update(new BasicDBObject(
new BasicDBObject(
"$set", new BasicDBObject("item","TBD")
)
));
BulkWriteResult writeResult = bulk.execute();
System.out.println(writeResult);
You can either fill in the DBObject types required by defining them, or use the builders supplied in the spring mongo library which should all support "extracting" the DBObject that they build.
public <T> void bulkUpdate(String collectionName, List<T> documents, Class<T> tClass) {
BulkOperations bulkOps = mongoTemplate.bulkOps(BulkOperations.BulkMode.UNORDERED, tClass, collectionName);
for (T document : documents) {
Document doc = new Document();
mongoTemplate.getConverter().write(document, doc);
org.springframework.data.mongodb.core.query.Query query = new org.springframework
.data.mongodb.core.query.Query(Criteria.where(UNDERSCORE_ID).is(doc.get(UNDERSCORE_ID)));
Document updateDoc = new Document();
updateDoc.append("$set", doc);
Update update = Update.fromDocument(updateDoc, UNDERSCORE_ID);
bulkOps.upsert(query, update);
}
bulkOps.execute();
}
Spring Mongo template is used to perform the update. The above code will work if you provide the _id field in the list of documents.
I have a mongo document that I am trying to update before insert in mongodb.
I have to put theses 3 keys
document._parentId = ObjectId()
document.aDictionnary.actionId = Integer
document.aDictionnary.bDictionnary.id = Integer
I have tried a few combinaison, but can't make it work.
Here is my current code
myClass.getDocument().append( "$set", new BasicDBObject().append("_parentId", myClass.getDocument.getId() ) );
myClass.getDocument().append( "$set", new BasicDBObject().append("aDictionnary", new BasicDBObject().append("actionId", actionToAttachId ) ) );
if( null == myClass.getSiteId() )
{
myClass.getDocument().append( "$set", new BasicDBObject().append("aDictionnary", new BasicDBObject().append("bDictionnary", new BasicDBObject().append( "id", actionToAttach.getSiteId() ))));
}
I don't want to directly update my document in database, reason is I keep all the history so each entry is a new insert.
Application doesn't crash, but fail on insert because of a wrong "append" syntax
Also I think it's unpleasing to write that kind of code because of the nested basicdbobject.append syntax, is there another way to do so ?
Here is the stack trace
163530 [http-8080-8] ERROR com.myapp.persistance.mystuff.MyClassMongo - java.lang.IllegalArgumentException: fields stored in the db can't start with '$' (Bad Key: '$set')
Have you tried the $push operator?
Example:
db.students.update({ _id: 1 },{ $push: { scores: 89 }})
In your example:
document._parentId = ObjectId()
document.aDictionnary.actionId = Integer
document.aDictionnary.bDictionnary.id = Integer
Essentially that equates to:
{_parentId: ObjectId(), aDictionary: {actionId: actionId,
bDictionnary: {id: id}}
So there are 3 documents - the toplevel all the way down to the nested bDictionnary. Each one of these is a DBObject so you'd need to build up the DBObject and save it as appropriate - here is some untested pseudo code:
DBObject myDocument = new BasicDBObject();
myDocument.put("_parentId", ObjectId())
DBObject aDictionary = new BasicDBObject("actionId", actionId)
.append("bDictionary", new BasicDBObject("id", id))
myDocument.put("aDictionary", aDictionary)
db.save(myDocument)
I'm using the Java driver with MongoDB. I have a List of document id's in a collection. I want to update a single field in every document that has an "_id" equal to one of the document id's in my List. In the below example, I tried something like this:
List<ObjectID> list = new ArrayList<ObjectID>();
list.append(new ObjectId("123"));
list.append(new ObjectId("456"));
list.append(new ObjectId("789"));
column.updateMulti(new BasicDBObject("_id", list),new BasicDBObject("$set",new BasicDBObject("field",59)));
My intentions are to update the documents with _id=123, _id=456 and _id=789, setting their "field" attribute to 59.
Am I going about this the right way?
I believe you need to make a couple changes:
BasicDBList list = new BasicDBList();
list.add( new ObjectId("123") );
// Add the rest...
DBObject inStatement = new BasicDBObject( "$in", list );
column.updateMulti( new BasicDBObject( "_id", inStatement ), new BasicDBObject( "$set", new BasicDBObject( "field", 59 ) );
Otherwise, with your current query, you're doing an equality comparison of the _id property against a list of _ids - not actually using the $in operator.