Mongoose: deep population (populate a populated field)
Asked Answered
T

10

113

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

Triptolemus answered 18/9, 2013 at 8:45 Comment(2)
like rroxysam said, .populate({path : 'userId', populate : {path : 'reviewId'}}).exec(...) Seems like a recursively logic and that make sense. it's works!Unthoughtof
Updates to Mongoose since this question has been posted have addressed this problem. Here is the documentation: Populating across multiple levelsSandbank
S
26

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

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

Scupper answered 7/3, 2014 at 14:58 Comment(0)
S
249

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

  })
Sacerdotalism answered 5/10, 2016 at 15:52 Comment(3)
If only I've read your answer and didn't waste an hour of my time on the answers above!Galactometer
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 documentPutout
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.Parallelogram
L
48

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

Lancewood answered 12/1, 2017 at 18:35 Comment(2)
Thank you, @Lancewood Your comment above populate instruction made me understand how populate works.Draught
Thankyou , best and simple answerExocentric
S
26

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

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

Scupper answered 7/3, 2014 at 14:58 Comment(0)
D
23

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.

Demise answered 10/12, 2014 at 19:35 Comment(0)
M
12

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) );
Marguerite answered 16/1, 2020 at 10:18 Comment(0)
A
11

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);
    });
});
Artemas answered 13/6, 2014 at 5:30 Comment(2)
I'm curious how you would do the same thing if user.favorites was an array?Torbernite
Same way. Just get rid of the .thingArtemas
S
7

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)
                            }
                   });
Strip answered 28/12, 2018 at 11:48 Comment(0)
C
5

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.

Conjuration answered 24/10, 2013 at 21:56 Comment(0)
P
3
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.

Patras answered 6/12, 2014 at 17:43 Comment(0)
A
2

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);
    }
  });
Axiom answered 8/12, 2019 at 15:43 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.