dor.avramov
dor.avramov

Reputation: 77

How to retrieve matching element in array in spring mongodb ?

Im trying to retrieve a document with a specific '_id' and a single embedded document with another specific '_id'.

my document is represent a catalog and it contains an array of courses.

example data:

'_id': ObjectId('1111'),
'name': 'example catalog',
...
...
'courses': [
     { 
         '_id': ObjectId('2222'),
         'name': 'my course',
         ...
     },
     {
         ....
     }

In mongod I run this aggregation query, and get back what I wish for:

db.getCollection('catalogs').aggregate(
{ $match: { '_id': ObjectId('58e8da206ca4f710bab6ef74') } },
{ $unwind: '$courses' },
{ $match: { 'courses._id': ObjectId('58d65541495c851c1703c57f') } })

As I mentioned earlier, I've get back I single catalog instance with a single course instance within.

In my java repo, I was trying to do the same:

    Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.match(Criteria.where(Catalog.ID_FIELD).is(catalogId)),
            Aggregation.unwind(Catalog.COURSES_FIELD, true),
            Aggregation.match(Criteria.where(Catalog.COURSES_FIELD + '.' + Course.ID_FIELD).is(embeddedCourseId))
    );
    AggregationResults<Catalog> results = mongoTemplate.aggregate(aggregation,
            Catalog.class, Catalog.class);

    List<Catalog> catalog  = results.getMappedResults();

But unfortunately, I've got an instance of my 'example catalog' with empty array of courses.

While debugging, I've found that inside results, there are two props that returns back. first one is what I've used, called mappedResults (represents the converted object returning from mongoDB) - contains an empty array of courses. the other one is the rawResults, (represents the data as DBObject) - contains the specific course I query for

my Catalog class contains an ArrayList (if that make any difference).

Please help and let me know what should I do to convert the results properly, or if I did something wrong in my code.

Upvotes: 7

Views: 20274

Answers (2)

s7vr
s7vr

Reputation: 75924

You can try below options. The key is to preserve the structure when mapping the response.

Regular Queries:

Using $positional projection

Query query = new Query();
query.addCriteria(Criteria.where("id").is(new ObjectId("58e8da206ca4f710bab6ef74")).and("courses.id").is(new ObjectId("58d65541495c851c1703c57f")));
query.fields().include("name").position("courses", 1);
List<Course> courses = mongoTemplate.find(query, Course.class);

Using $elemMatch projection

Query query = new Query();
query.addCriteria(Criteria.where("id").is(new ObjectId("58e8da206ca4f710bab6ef74")));
query.fields().include("name").elemMatch("courses", Criteria.where("_id").is(new ObjectId("58d65541495c851c1703c57f") ) );
List<Course> Course = mongoTemplate.find(query, Course.class);

Aggregation

Mongo Version >= 3.4 & Spring 1.5.2 Boot / Spring 1.10.1 Mongo.

You can use $addFields stage which will overwrite the courses field with the $filter value while keeping all the existing properties. I couldn't find any addFields builder in current spring version. So I have to use AggregationOperation to create a new one.

AggregationOperation addFields = new AggregationOperation() {
    @Override
    public DBObject toDBObject(AggregationOperationContext aggregationOperationContext) {
        DBObject dbObject =
                new BasicDBObject("courses",
                        new BasicDBObject("$filter",
                                new BasicDBObject("input", "$$courses").
                                        append("as", "course").
                                        append("cond",
                                            new BasicDBObject("$eq", Arrays.<Object>asList("$$course._id", new ObjectId("58d65541495c851c1703c57f"))))));
        return new BasicDBObject("$addFields", dbObject);
    }
};

Aggregation aggregation = Aggregation.newAggregation(
            Aggregation.match(Criteria.where("_id").is(new ObjectId("58e8da206ca4f710bab6ef74"))),
            addFields
 );

Mongo Version = 3.2 & Spring 1.5.2 Boot / Spring 1.10.1 Mongo..

The idea is still same as above but this pipeline uses $project so you'll have to add all the fields that you want to keep in final response. Also used spring helper methods to create the $filter pipeline.

Aggregation aggregation = newAggregation(
     Aggregation.match(Criteria.where("id").is(new ObjectId("58e8da206ca4f710bab6ef74"))),
     Aggregation.project("name")
                 .and(ArrayOperators.Filter.filter("courses").as("course")                          
                 .by(ComparisonOperators.Eq.valueOf("course._id").equalToValue(new ObjectId("58d65541495c851c1703c57f")))
                    ).as("courses")
 );

Mongo Version <= 2.6

You'll have to use $unwind and add a course field to have spring map it correctly.

Upvotes: 6

helmy
helmy

Reputation: 9497

The problem that you have here is that your Catalog class has a courses field which maps to a List/ArrayList. But when your aggregation query unwinds the courses array, it is going to output the courses field as a sub-document. The Spring mapper doesn't know how to deal with that because it doesn't match your Catalog object structure.

You haven't fully defined your problem here, but what would probably make more sense is if you had the aggregation return a Course object rather than a Catalog object. In order to do that you're going to need to add a projection stage to your aggregation pipeline so that the result looks exactly like a single Course object. The key is that the data coming back from MongoDB needs to match your object structure.

Upvotes: 0

Related Questions