Mongoose populate() method

Mongoose populate() method

·

4 min read

Prerequisites :

  • Basics of MongoDB ( What are NoSQL databases, documents etc).

  • Basics of mongoose ( schemas, models, basic querying ).

Why learn it ?

Creating relations between data is an important part of a database. We use references in mongoose ( mongoDB ) to create a relation between collections. For example, we have one collection for car makers and another collections for cars. We can reference the car maker in the car schema.

Code -

const carSchema = new mongoose.Schema({
  // Other fields of the car schema...
  maker: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Maker'  // This should match the name of your Maker model
  }
});
const Car = mongoose.model('Car', carSchema);

One important thing to note is when we query from the car collection, by default only the maker's id will be returned in the maker field .

// car document
 {
  "_id": ObjectId("61d54e6d1db2602c307f81e2"),
  "model": "Corolla",
  "year": 2022,
  "maker": ObjectId("61d54dbf1db2602c307f81e1")
}

// document of maker of above car 

{
  "_id": ObjectId("61d54dbf1db2602c307f81e1"),
  "name": "Toyota",
  "country": "Japan",
  "founded": "1937",
}

Populate comes into picture...

But what if you also want the details of the maker when you query for cars. populate() method helps us in doing just that.

const carWithMaker = await Car.findById(carId).populate('maker');

So now, the 'carWithMaker' will have the actual object ( document ) of car maker instead of just it's ID.

Populate method in detail :

Parameters :

You can either pass the field in the document that you want to populate with data from another collection as shown above. Optionally, you can pass an object to populate() to specify additional options like select, match, options, and model. For example :

populate({
  path: 'fieldToPopulate',
  select: 'field1 field2', 
  match: { condition: value },
  options: { sort: { field: 1 } },
  model: 'ModelName'
})

Let's understand each of these additional options :

  • path - The name of the field you want to populate. E.g the maker field in the initial example.

  • select - What all fields of the document you want in the referred collection ( E.g in car maker you may need any/all of name, country, founded fields. )

Before we move on to 'match' and 'options'. Let's take a more complex example.

const userSchema = new mongoose.Schema({
  username: String,
  email: String,
  role: String
});

const postSchema = new mongoose.Schema({
  title: String,
  content: String,
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  },
  comments: [{
    type: mongoose.Schema.Types.ObjectId,
    ref: 'Comment'
  }]
});

const commentSchema = new mongoose.Schema({
  content: String,
  author: {
    type: mongoose.Schema.Types.ObjectId,
    ref: 'User'
  }
},{
  timestamps: true
});

const User = mongoose.model('User', userSchema);
const Post = mongoose.model('Post', postSchema);
const Comment = mongoose.model('Comment', commentSchema);

In above example, Post model has a reference to User and the Comment model ( there can be multiple comments for a single post ). Also, the Comment model itself has a reference to the User model ( nested reference ).

  • match - The match option is particularly useful when you want to filter the populated documents based on specific field values. E.g
const postId = '604a20c7c5487e10a4ab9f1d'; // Example post ID

const post = await Post.findById(postId)
  .populate({
    path: 'userId',
    match: { role: 'admin' }  // 
  })

which filters users based on the role "admin"

  • options - Provides additional options that affect the behavior of the population process. Common options include sort, limit, skip, and maxTimeMS, among others. These options allow you to control aspects such as sorting the documents to populate, the maximum number of documents to populate, the number of documents to skip, and the maximum time to wait for the operation to complete. E.g

      const postId = '604a20c7c5487e10a4ab9f1d'; // Example post ID
    
      const post = await Post.findById(postId)
        .populate({
          path: 'comments',
          options: { sort: { createdAt: -1 }, limit: 5  }
        })
    

which sorts the comments by creation date in descending order and limits number of comments to 5.

  • model - Specifies the model to use for populating the field. This option allows you to specify a different model than the one referenced in the schema. It's useful when you want to populate a field with documents from a different collection or model.

    By default, when you define a reference field in a Mongoose schema using ref, Mongoose assumes that the referenced documents belong to the model specified in the ref option. For example:

      const postSchema = new mongoose.Schema({
        author: {
          type: mongoose.Schema.Types.ObjectId,
          ref: 'User' // Reference to the User model
        }
      });
    

    However, you can override this default behavior by explicitly specifying a different model to use for populating the field using the model option.

        const result = await Post.findById(postId)
        .populate({
          path: 'author',
          model: 'AdminUser' // Populate 'author' field with documents from the 'AdminUser' model
        })
    

Chaining :

  • You can chain multiple populate() calls to populate multiple fields in a document.

  • Nested fields can also be populated by specifying the path using dot notation.

    E.g :

const postId = '604a20c7c5487e10a4ab9f1d'; // Example post ID

const post = await Post.findById(postId)
  .populate('author') // Populate the 'author' field
  .populate('comments.author') // Populate the 'comments.author' field

Conclusion :

populate() is a powerful feature in Mongoose that simplifies working with related data across multiple collections in MongoDB. It's commonly used in scenarios where you have relational data and need to fetch related documents efficiently.