111

I have Category model:

Category:
    ...
    articles: [{type:ObjectId, ref:'Article'}]

Article model contains ref to Account model.

Article:
    ...
    account: {type:ObjectId, ref:'Account'}

So, with populated articles Category model will be:

{ //category
    articles: //this field is populated
     [ { account: 52386c14fbb3e9ef28000001, // I want this field to be populated
         date: Fri Sep 20 2013 00:00:00 GMT+0400 (MSK),
         title: 'Article 1' } ],
    title: 'Category 1' }

The questions is: how to populate subfield (account) of a populated field ([articles])? Here is how I do it now:

globals.models.Category
    .find
        issue : req.params.id
        null
        sort:
            order: 1
    .populate("articles") # this populates only article field, article.account is not populated
    .exec (err, categories) ->
        console.log categories

I know it was discussed here: Mongoose: Populate a populated field but no real solution was found

Community
  • 1
  • 1
f1nn
  • 6,989
  • 24
  • 69
  • 92
  • 3
    like rroxysam said, `.populate({path : 'userId', populate : {path : 'reviewId'}}).exec(...)` Seems like a recursively logic and that make sense. it's works! – Ewertom Moraes Apr 30 '17 at 03:48
  • Updates to Mongoose since this question has been posted have addressed this problem. Here is the documentation: [**Populating across multiple levels**](https://mongoosejs.com/docs/populate.html#deep-populate) – Chunky Chunk Aug 17 '19 at 02:46

10 Answers10

244

Firstly, update mongoose 3 to 4 & then use the simplest way for deep population in mongoose as shown below:

Suppose you have Blog schema having userId as ref Id & then in User you have some review as ref Id for schema Review. So Basically, you have three schemas:

  1. Blog
  2. User
  3. Review

And, you have to query from blog, which user owns this blog & the user review. So you can query your result as :

BlogModel
  .find()
  .populate({
    path : 'userId',
    populate : {
      path : 'reviewId'
    }
  })
  .exec(function (err, res) {

  })
Eldar B.
  • 1,097
  • 9
  • 22
Abhay
  • 6,410
  • 4
  • 26
  • 34
  • 14
    If only I've read your answer and didn't waste an hour of my time on the answers above! – Amin Jafari Jan 06 '17 at 20:38
  • 2
    How would you populate the second level when you want to populate two fields? It keeps only returning the last field for me when I use select: since I only want to get back specific fields in the second level document – bzlight May 01 '19 at 18:02
  • 5
    Just in case anyone is wondering if you want to populate several paths you can pass in arrays of objects as such : .populate([ { path: 'path_1', populate: { path: 'field_1' } }, { path: 'path_2', populate: [{ path: 'field_1' }, { path: 'field_2' }] } ]).exec(). Also note that giving simple strings in the array will not work as it usually does. Use { path: 'field' } structure instead. – virtual_Black_Whale Nov 02 '20 at 10:59
47

Populating across multiple levels

Say you have a user schema which keeps track of the user's friends.

var userSchema = new Schema({
  name: String,
  friends: [{ type: ObjectId, ref: 'User' }]
});

Populate lets you get a list of a user's friends, but what if you also wanted a user's friends of friends? Specify the populate option to tell mongoose to populate the friends array of all the user's friends:

User.findOne({ name: 'Val' }).populate({
    path: 'friends',
    // Get friends of friends - populate the 'friends' array for every friend
    populate: { path: 'friends' }
});

Reference: http://mongoosejs.com/docs/populate.html#deep-populate

l-l
  • 3,804
  • 6
  • 36
  • 42
Lucas
  • 503
  • 5
  • 6
25

Mongoose has now a new method Model.populate for deep associations:

https://github.com/Automattic/mongoose/issues/1377#issuecomment-15911192

hong4rc
  • 3,999
  • 4
  • 21
  • 40
DestyNova
  • 726
  • 8
  • 21
22

It might be a bit too late, but I wrote a Mongoose plugin to perform deep population at any arbitrary nested levels. With this plugin registered, you can populate category's articles and accounts with just a single line:

Category.deepPopulate(categories, 'articles.account', cb)

You can also specify populate options to control things like limit, select... for each populated path. Checkout the plugin documentation for more information.

Buu
  • 49,745
  • 5
  • 67
  • 85
11

Or you can pass Object to the populate method as:

const myFilterObj = {};
const populateObj = {
                path: "parentFileds",
                populate: {
                    path: "childFileds",
                    select: "childFiledsToSelect"
                },
                select: "parentFiledsToSelect"
               };
Model.find(myFilterObj)
     .populate(populateObj).exec((err, data) => console.log(data) );
Ravi Singh
  • 1,049
  • 1
  • 10
  • 25
10

Easiest way to accomplish this in 3.6 is to use Model.populate.

User.findById(user.id).select('-salt -hashedPassword').populate('favorites.things').exec(function(err, user){
    if ( err ) return res.json(400, err);

    Thing.populate(user.favorites.things, {
        path: 'creator'
        , select: '-salt -hashedPassword'
    }, function(err, things){
        if ( err ) return res.json(400, err);

        user.favorites.things = things;

        res.send(user.favorites);
    });
});
Rahil Wazir
  • 10,007
  • 11
  • 42
  • 64
chovy
  • 72,281
  • 52
  • 227
  • 295
6

This concept is deep Population. Here Calendar,Subscription,User,Apartment are mongoose ODM models in different levels

Calendar.find({}).populate({
      path: 'subscription_id',model: 'Subscription',
         populate: {path: 'user_id',model: 'User',
           populate: {path: 'apartment_id',model: 'Apartment',
              populate: {path: 'caterer_nonveg_id',
                          model: 'Caterer'}}}}).exec(function(err,data){ 
                          if(!err){
                             console.log('data all',data)
                           }
                           else{
                             console.log('err err err',err)
                            }
                   });
coDe murDerer
  • 1,858
  • 4
  • 20
  • 28
4

Sorry to burst your bubble, but there's not a directly supported solution to this. As for Github issue #601, it looks grim. According to the 3.6 release notes, it looks like the developers acknowledged the issue are happy with manual recursive/deep population.

So from the release notes, the recommended method is to nest populated calls in the callback, so in your exec() function, use categories.populate to further populate before sending a response.

Kevin Wang
  • 3,290
  • 1
  • 28
  • 40
2
globals.models.Category.find()
  .where('issue', req.params.id)
  .sort('order')
  .populate('articles')
  .exec(function(err, categories) {

    globals.models.Account.populate(categories, 'articles.account', function(err, deepResults){

      // deepResult is populated with all three relations
      console.log(deepResults[0].articles[0].account);

    });
});

The following example is inspired by the question asked @codephobia and populates two levels of many relationships. First fetch a user, populate its array of related orders and include each orderDetail.

user.model.findOne()
  .where('email', '***@****.com')
  .populate('orders')
  .exec(function(err, user) {

    orderDetail.model.populate(user, 'orders.orderDetails', function(err, results){

      // results -> user.orders[].orderDetails[] 
    });
});

This works fine in 3.8.8 but should work in 3.6.x.

Eat at Joes
  • 4,937
  • 1
  • 40
  • 40
1

If you want select multi populate inside populate, you should try this way:

I have Booking schema:

let Booking = new Schema({
  ...,  // others field of collection
  experience: { type: Schema.Types.ObjectId, ref: 'Experience' },
  ...},{
    collection: 'booking'
  });

and Experience schema:

let Experience = new Schema({
  ...,
  experienceType: {type: Schema.Types.ObjectId, ref: 'ExperienceType'},
  location: {type: Schema.Types.ObjectId, ref: 'Location'},
  ...} // others field of collection
  ,{
    collection: 'experience'
  });

get ExperienceType and Location of Experience when you find Booking:

Booking.findOne({_id: req.params.id})
  .populate({path: 'experience',
    populate: [{path: 'experienceType', select: 'name'}, {path: 'location', select: 'name'}],
  })
  .exec((err, booking) => {
    if(err){
      console.log(err);
    }
    else {
      res.json(booking);
    }
  });
yyater97
  • 1,697
  • 2
  • 9
  • 5