How to mix Spring Data queries and MongoDB syntax
I love writing queries using Spring Data’s Criteria. Writing queries directly in SQL (@NativeQuery, @Query) feels a bit dirty. And I also think the official API using MongoCollection and Bson is quirk (at least for a Java developers).
I started using MongoDB a year ago, so I feel pretty lost at first. I started using Spring’s Criteria because it felt more usable and I felt more skilled. But as every abstraction, it is not perfect and does not cover all cases. So, what happens when Spring’s DSL is not powerful enough to describe some part of your query? I had to write all my stages using Bson documents because I didn’t know how to mix Spring’s and MongoDb’s objects.
For example, I was unable to write an addField operation using AggregationOperations. Besides, I had to write a pretty complex expression with operators precedence. I search stackoverflow, indeed, what didn’t give me the solution but gave me some hints O:)
Spring is usually well written and relatively easy to extend/tune to your case. So that… why don’t writing our custom AggregationOperation? Here you have a snippet of my proposal.
/**
* This example shows how to mix Spring Data Criteria with stages written in MongoDB syntax.
*/
@Component
public class MongoFilterExample {
@Nonnull
private MongoTemplate mongoTemplate;
public MongoFilterExample(@Nonnull MongoTemplate mongoTemplate) {
this.mongoTemplate = mongoTemplate;
}
public AggregationResults<MatchDocument> example() {
// Using Spring syntax
Criteria match = new Criteria().where("id").is("document-id");
Sort sort = Sort.by("creationDate");
// Using Spring's AggregationOperation built with MongoDB syntax
FieldsExposingAggregationOperation addFields = addFieldsOperation();
return aggregate(match, addFields, sort);
}
/**
* It receives standard Spring's Criteria and Sort, and a Spring's AggregationOperation to be use with the Spring's pipeline operation.
*/
@Nonnull
public AggregationResults<MatchDocument> aggregate(@Nonnull Criteria match, @Nonnull AggregationOperation addFieldsOperation, @Nonnull Sort sort) {
MatchOperation matchOperation = Aggregation.match(match);
SortOperation sortOperation = Aggregation.sort(sort);
return aggregate(matchOperation, addFieldsOperation, sortOperation);
}
/**
* Aggregation using Spring's MongoTemplate syntax.
*/
@Nonnull
public AggregationResults<FancyDocument> aggregate(@Nonnull AggregationOperation... pipeline) {
TypedAggregation<FancyDocument> aggregation =
Aggregation.newAggregation(FancyDocument.class, pipeline);
return mongoTemplate.aggregate(aggregation, FancyDocument.class);
}
@Nonnull
public FieldsExposingAggregationOperation addFieldsOperation() {
return
new FieldsExposingAggregationOperation() {
@Override
public Document toDocument(AggregationOperationContext context) {
// Writing MongoDB queries here, so that use the names of the fields as stored in MongoDB
Bson newField = sqr(minus("$anotherField", 1));
// Returning a MongoDB stage here: {"$addFields: {...}}
return new Document("$addFields", new Document("newField", newField));
}
@Override
public ExposedFields getFields() {
Field f = Fields.field("newField");
return ExposedFields.synthetic(Fields.from(f));
}
@Override
public boolean inheritsFields() {
return true;
}
};
}
/**
* Helper methods that write operations in MongoDB syntax.
*/
@Nonnull
private Bson sqr(@Nonnull Bson o) {
BasicDBList list = new BasicDBList();
list.add(o);
list.add(2);
return new BasicDBObject("$pow", list);
}
@Nonnull
private Bson minus(@Nonnull String field, double value) {
BasicDBList minusArgs = new BasicDBList();
minusArgs.add(field);
minusArgs.add(-value);
return new BasicDBObject("$add", minusArgs);
}
}