38

I've just got stuck with this problem. I've got two Mongoose schemas:

var childrenSchema = mongoose.Schema({
    name: {
        type: String
    },
    age: {
        type: Number,
        min: 0
    }
});

var parentSchema = mongoose.Schema({
    name : {
        type: String
    },
    children: [childrenSchema]
});

Question is, how to fetch all subdocuments (in this case, childrenSchema objects) from every parent document? Let's suppose I have some data:

var parents = [
    { name: "John Smith",
    children: [
        { name: "Peter", age: 2 }, { name: "Margaret", age: 20 }
    ]},
    { name: "Another Smith",
    children: [
        { name: "Martha", age: 10 }, { name: "John", age: 22 }
    ]}
];

I would like to retrieve - in a single query - all children older than 18. Is it possible? Every answer will be appreciated, thanks!

Nuri Tasdemir
  • 9,720
  • 3
  • 42
  • 67
Kuba T
  • 2,893
  • 4
  • 25
  • 30
  • Do you want it to only return the parent if the child is over 18 or do you want it to only populate the children that are over 18 on each parent? – mr.freeze May 30 '13 at 20:48
  • It would be great if I get the set of "children"... – Kuba T May 30 '13 at 21:01

3 Answers3

61

You can use $elemMatch as a query-projection operator in the most recent MongoDB versions. From the mongo shell:

db.parents.find(
    {'children.age': {$gte: 18}},
    {children:{$elemMatch:{age: {$gte: 18}}}})

This filters younger children's documents out of the children array:

{ "_id" : ..., "children" : [ { "name" : "Margaret", "age" : 20 } ] }
{ "_id" : ..., "children" : [ { "name" : "John", "age" : 22 } ] }

As you can see, children are still grouped inside their parent documents. MongoDB queries return documents from collections. You can use the aggregation framework's $unwind method to split them into separate documents:

> db.parents.aggregate({
    $match: {'children.age': {$gte: 18}}
}, {
    $unwind: '$children'
}, {
    $match: {'children.age': {$gte: 18}}
}, {
    $project: {
        name: '$children.name',
        age:'$children.age'
    }
})
{
    "result" : [
        {
            "_id" : ObjectId("51a7bf04dacca8ba98434eb5"),
            "name" : "Margaret",
            "age" : 20
        },
        {
            "_id" : ObjectId("51a7bf04dacca8ba98434eb6"),
            "name" : "John",
            "age" : 22
        }
    ],
    "ok" : 1
}

I repeat the $match clause for performance: the first time through it eliminates parents with no children at least 18 years old, so the $unwind only considers useful documents. The second $match removes $unwind output that doesn't match, and the $project hoists children's info from subdocuments to the top level.

A. Jesse Jiryu Davis
  • 23,641
  • 4
  • 57
  • 70
  • It works! :) Thanks for help! :) Where can I get some docs about advanced MongoDB operations? Its docs are enough? I wish to learn as much as possible about MongoDB :) – Kuba T Jun 02 '13 at 18:21
  • 1
    Take 10gen's online MongoDB For Developers class, read Rick Copeland's MongoDB Design Patterns book. – A. Jesse Jiryu Davis Jun 03 '13 at 16:00
  • 1
    I know, i'm quite late, but what if a parent have more than one children over 18? I tried the first query on mongo shell, but it only retrieves one match for each parent. Am I right? – andrea.spot. Sep 25 '14 at 15:50
  • Thanks!@A.JesseJiryuDavis I have one more question about it, I saw the results only contain "_id" of parent object, what could I do if I want keep all parent's data and only with children over 18, like: { "_id" : ..., "name": "John Smith", "children" : [ { "name" : "Margaret", "age" : 20 } ] } – Jiang Apr 14 '15 at 20:32
  • For those who end up here and are frustrated with this answer like I was... It can be done just using find. Here's an example: https://mongoplayground.net/p/qfAskjZcNGz I got there from this question, though it's specifically mentioning nesting. https://stackoverflow.com/questions/36229123/return-only-matched-sub-document-elements-within-a-nested-array – Ethan Jan 05 '22 at 02:53
23

In Mongoose, you can also use the elegant .populate() function like this:

parents
.find({})
.populate({
  path: 'children',
  match: { age: { $gte: 18 }},
  select: 'name age -_id'
})
.exec()
AbdelHady
  • 9,334
  • 8
  • 56
  • 83
1

A. Jesse Jiryu Davis's response works like a charm, however for later versions of Mongoose (Mongoose 5.x) we get the error:

Mongoose 5.x disallows passing a spread of operators to Model.aggregate(). Instead of Model.aggregate({ $match }, { $skip }), do Model.aggregate([{ $match }, { $skip }])

So the code would simply now be:

> db.parents.aggregate([{
    $match: {'children.age': {$gte: 18}}
}, {
    $unwind: '$children'
}, {
    $match: {'children.age': {$gte: 18}}
}, {
    $project: {
        name: '$children.name',
        age:'$children.age'
    }
}])
{
    "result" : [
        {
            "_id" : ObjectId("51a7bf04dacca8ba98434eb5"),
            "name" : "Margaret",
            "age" : 20
        },
        {
            "_id" : ObjectId("51a7bf04dacca8ba98434eb6"),
            "name" : "John",
            "age" : 22
        }
    ],
    "ok" : 1
}

(note the array brackets around the queries)

Hope this helps someone!

cjwiseman11
  • 74
  • 1
  • 6