21

I would like to retrieve a sub document from a document in MongoDB. I have the following document:

{
    "_id" : "10000",
    "password" : "password1",
    "name" : "customer1",
    "enabled" : true,
    "channels" : [ 
        {
            "id" : "10000-1",
            "name" : "cust1chan1",
            "enabled" : true
        }, 
        {
            "id" : "10000-2",
            "name" : "cust1chan2",
            "enabled" : true
        }
    ]
}

The result I would like is:

{
    "id" : "10000-1",
    "name" : "cust1chan1",
    "enabled" : true
}

However, the best I can do so far is using the following query:

db.customer.find({"channels.id" : "10000-1"}, {"channels.$" : 1, "_id" : 0})

But this gives me the following result:

{
    "channels" : [ 
        {
            "id" : "10000-1",
            "name" : "cust1chan1",
            "enabled" : true
        }
    ]
}

Does anyone know if it is possible to write a query that will give me my desired result? Any help would be much appreciated.

2
  • If you're doing it from the mongoshell, something like db.customer.find({"channels.id" : "10000-1"}, {"channels.$" : 1, "_id" : 0}).channels[0] should do it Commented Jan 14, 2014 at 12:36
  • Thanks for the suggestion. I'm using the spring mongoTemplate (java driver) so I don't think this will work for me. I tried this in mongo shell and I get the error: TypeError: Cannot read property '0' of undefined. Commented Jan 14, 2014 at 12:47

3 Answers 3

14

You can do it with Aggregation Framework. Query will be something like :

db.customer.aggregate([
    {$unwind : "$channels"},
    {$match : {"channels.id" : "10000-1"}},
    {$project : {_id : 0, 
                 id : "$channels.id", 
                 name : "$channels.name", 
                 enabled : "$channels.enabled"}}
])
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for replying so quickly. This is close but it's not exactly what I need. This gives me: { "result" : [ { "name" : "cust1chan1", "enabled" : true, "id" : "10000-1" } ], "ok" : 1 } rather than: { "id" : "10000-1", "name" : "cust1chan1", "enabled" : true }
I don't think this is possible to get what you want. Why don't you want to extract fields from the result on the application layer?
I think you're right that it's not possible. Yes, I can extract the fields but it's just nicer and cleaner to not have to do that. I posted this question because I looked over the Mongo documentation and I don't see any reference to doing this so I was wondering if anyone else had done it. An alternative could be to put a reference from one document to another subdocument(s) and store them in separate collections but then you have the issue of Mongo not supporting transactions.
I think you can use $cond in $project to get it done, not sure though
3

Using MongoDB 3.4.4 and newer, the aggregation framework offers a number of operators that you can use to return the desired subdocument.

Consider running an aggregate pipeline that uses a single $replaceRoot stage to promote the filtered subdocument to the top-level and replace all other fields.

Filtering the subdocument requires the $filter operator which selects a subset of an array to return based on the specified condition i.e. returns an array with only those elements that match the condition. You can then convert the single array element to a document by using the $arrayElemAt operator

Overall running this aggregate operation will yield the desired result:

db.customer.aggregate([
    { "$replaceRoot": { 
        "newRoot": {
            "$arrayElemAt": [
                { "$filter": {
                   "input": "$channels",
                   "as": "channel",
                   "cond": { /* resolve to a boolean value and determine if an element should be included in the output array. */
                       "$eq": ["$$channel.id", "10000-1"]
                    } 
                } },
                0 /* the element at the specified array index */
            ]
        }
    } }
])

Output

{
    "id" : "10000-1",
    "name" : "cust1chan1",
    "enabled" : true
}

Comments

0

I know it may be a bit late, but I was in a similar situation and I figure out the solutions for this. Inside the aggregation pipeline, you can first filter out the data by matching sub-documents and then just unwind the nested array field and then replace the root.

db.customer.aggregate([
    [
        { $match: { "channels.id": "10000-1" }},
        { $unwind: "$channels" },
        { $replaceRoot: { newRoot: "$channels" } }
    ]
])

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.