Sling Academy
Home/Node.js/Mongoose: How to populate a subdocument after creation

Mongoose: How to populate a subdocument after creation

Last updated: December 30, 2023

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!

Next Article: MongooseJS: Cursor-Based Pagination Examples

Previous Article: Mongoose: Remove password field from query results

Series: Mongoose.js Tutorials

Node.js

You May Also Like

  • NestJS: How to create cursor-based pagination (2 examples)
  • Cursor-Based Pagination in SequelizeJS: Practical Examples
  • MongooseJS: Cursor-Based Pagination Examples
  • Node.js: How to get location from IP address (3 approaches)
  • SequelizeJS: How to reset auto-increment ID after deleting records
  • SequelizeJS: Grouping Results by Multiple Columns
  • NestJS: Using Faker.js to populate database (for testing)
  • NodeJS: Search and download images by keyword from Unsplash API
  • NestJS: Generate N random users using Faker.js
  • Sequelize Upsert: How to insert or update a record in one query
  • NodeJS: Declaring types when using dotenv with TypeScript
  • Using ExpressJS and Multer with TypeScript
  • NodeJS: Link to static assets (JS, CSS) in Pug templates
  • NodeJS: How to use mixins in Pug templates
  • NodeJS: Displaying images and links in Pug templates
  • ExpressJS + Pug: How to use loops to render array data
  • ExpressJS: Using MORGAN to Log HTTP Requests
  • NodeJS: Using express-fileupload to simply upload files
  • ExpressJS: How to render JSON in Pug templates