spring,  mongodb,  tech,  pills

How to mix Spring Data queries and MongoDB syntax

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

    }
}