deep virtual population in mongoose is actually very simple!

mongoose’s virtual population is very useful tool. You can mention population path in schema as a virtual field and mongoose will automatically query and push all virtual documents for you while find query (and find like queries) by using populate method on query builder.

But there is a catch.

Let’s say you have a blog collection where resides lots of blogs. Blog schema have a virtually populated field author.

blogSchema = new mongoose.Schema({});
blogSchema.virtual('author', {
ref : 'User', // fetch from User model
localField : 'authorId',
foreignField : 'userId',
justOne : true
})

In user schema, we have a virtual path photos which is used to fetch photos of user.

userSchema = new mongoose.Schema({});
userSchema.virtual('photos', {
ref : 'Photo', // fetch from Photo model
localField : 'userId', // ↓
foreignField : 'uploaderId'
})

Now, when you fetch blogs and populate author like below…

blogModel.find({}).populate('author').exec(callback);

..you kinda also want author document to have photos in it, but author document (user document) is not populated (or asked to populate photos path), it will have photo field with null value.

Also, once use can scenario is, when you create a fresh blog document, it won’t have author field, as document created wasn’t populated explicitly.

So, how to get these things done?

Gladly, mongoose have populate prototype method for model and it’s instances (document). But they work differently than when used on query builder.

1. On document

case 1 : Fresh document

When we create blog document, we want to populate author path. Use populate method on document with pathnameas first parameter and callback as second parameter. You can also use Object format as first parameter with different options as mentioned in http://mongoosejs.com/docs/api.html#document_Document-populate

var blogDoc = new BlogModel({...});
//blogDoc.author === null
blogDoc.populate('author', function(err, document){
//blogDoc.author === {...}
});

As we know, populate author document will have have photos path to populate. Like above, author sub-document is also a valid mongoose document, so we can also use populate method on it like…

var blogDoc = new BlogModel({...});
blogDoc.populate('author', function(err, document){
//document.author.photos === null
    document.author.populate('photos', function(err, _document){
//_document.author.photos === [...]
});
});

case 2: queried document

When you query a documents or documents, you can follow above approach pretty much the same for sub-sub document. For sub document, you can populate right in the query phase like

blogModel.findOne({}).populate('author').exec(callback);
while populating a path, you must pass a callback as second parameter, else population will not execute.

2. On model

When you have list of documents fetched from a query like below..

blogModel.find({}).populate('author').exec(callback);

you can able to populate author path but not photos path inside author sub document. If you follow document populate approach like discussed earlier, you need to use forEach function and populate sub-sub document inside a loop.

Using populate method on model, you can avoid this loop. Mongoose provide populate prototype method on model which will accept list of documents to populate, second parameter as Object with options like path to populate and third argument as a callback function.

For this example, let’s say that we didn’t populate author path while querying

blogModel.find({}).exec(callback);

So now we have array have documents which needs to be populated with author sub-document.

Invoke populate method on blogModel and pass all documents. use path as author for options. You don’t need to mention anything else as we already have all important bits configured in the schema itself. Use final callback which will return docs with populated author path.

blogModel.find({}).exec(function(err, docs){
//docs[0].author == null
    blogModel.populate(docs, {path:'author'}, function(err, _docs){
//_docs[0].author == {}
})
});

Thing I have noticed is that even in the callback of populate function both on document or model, it has second parameter which returns the populate documents, original documents also get changed. That means populate methods mutates original documents. Hence, second parameter is optional but recommend to use to avoid confusion.