Mongoose: How to populate a subdocument after creation

Updated: December 30, 2023 By: Guest Contributor Post a comment

Overview

In MongoDB, references between data are generally resolved either through manual references, where you might store the ObjectId of one document within another, or through the use of Mongoose subdocuments and population features. The latter method is much more object-oriented and enables seamless retrieval of related data. This guide will tackle the Mongoose populate method, specifically focusing on how to populate a subdocument after it has already been created, which is a common scenario in building robust MongoDB applications with Node.js and Mongoose.

Basic Concept of Population

To understand population, you first need to wrap your head around references in Mongoose. There are two primary ways to embed documents inside one another:

  • Using an array
  • As a subdocument within a parent document

Population is a process to automatically replace the specified paths in the document with document(s) from other collections.

const ParentSchema = new Schema({
   child: { type: Schema.Types.ObjectId, ref: 'Child' }
 });

Fundamentals of Creating Subdocuments

Let’s construct a relationship between two models: Author and Post.

const authorSchema = new Schema({
   name: String,
   age: Number
 });

const postSchema = new Schema({
title: String,
content: String,
author: { type: Schema.Types.ObjectId, ref: 'Author' }
});

Generating Models and Creating Initial Data

With our Author and Post schemas established, we translate them into models and proceed to create an initial instance of an author and a post.

const Author = mongoose.model('Author', authorSchema);
const Post = mongoose.model('Post', postSchema);

async function createInitialData() {
  const author = new Author({ name: 'Jane Doe', age: 28 });
  await author.save();

  const post = new Post({
     title: 'Mongoose Guide',
     content: 'Understanding subdocuments and population.',
     author: author._id
  });
  await post.save();
}

Populating After Creation

Moving forward, let’s populate the author subdocument in the post we just created and retrieve it with all of its details.

async function populatePostAuthor(postId) {
   const postWithAuthor = await Post.findById(postId).populate('author').exec();
   console.log(postWithAuthor);
 }
 

Dealing with Nested Subdocuments

Things start to get more complex with multiple levels of subdocuments. For example, let’s expand our schema to include comments on each post, which reference user data.

const userSchema = new Schema({
   username: String,
   profilePicUrl: String
 });

postSchema.add({
  comments: [{
    text: String,
    user: { type: Schema.Types.ObjectId, ref: 'User' }
  }]
});

Then, the corresponding population operation for a post including its author and users commenting will be:

async function populatePostWithComments(postId) {
   const post = await Post.findById(postId)
     .populate('author')
     .populate({
       path: 'comments.user',
       model: 'User'
     }).exec();
   console.log(post);
 }
 

Handling Deep Population

Deep population involves populating a chain of connected documents. Considering posts that have comments, and these comments have replies:

commentSchema.add({
   replies: [{
     text: String,
     user: { type: Schema.Types.ObjectId, ref: 'User' }
   }]
 });

// To perform a deep population of comments and their replies:
async function populateDeeplyNestedComments(postId) {
    const postWithEverything = await Post.findById(postId).populate({
       path: 'comments.replies.user',
       model: 'User',
}).exec();
    console.log(postWithEverything);
}

Advanced Concerns

Handling errors correctly, and using query options like select, match, and sort is imperative to fine-tuning the populate feature effectively.

Post.findById(postId)
   .populate({
     path: 'comments.user',
     select: 'username profilePicUrl',
     match: { isActive: true },
     options: { sort: { username: -1 } }
   })
   .exec();
 

Conclusion

With this knowledge, you’re now fully equipped to handle both straightforward and complex population tasks with subdocuments in Mongoose. Always remember, optimizing your queries to fetch only necessary data is critical for the performance of your Node.js with Mongoose applications. Happy coding!