Population is the process of automatically replacing the specified paths in the document with document(s) from other collection(s). We may populate a single document, multiple documents, a plain object, multiple plain objects, or all objects returned from a query. Let’s look at some examples.

    So far we’ve created two . Our Person model has its stories field set to an array of ObjectIds. The ref option is what tells Mongoose which model to use during population, in our case the Story model. All _ids we store here must be document _ids from the Story model.

    Note: ObjectId, Number, String, and Buffer are valid for use as refs. However, you should use ObjectId unless you are an advanced user and have a good reason for doing so.

    Saving refs to other documents works the same way you normally save properties, just assign the _id value:

    1. const author = new Person({
    2. _id: new mongoose.Types.ObjectId(),
    3. name: 'Ian Fleming',
    4. age: 50
    5. });
    6. author.save(function (err) {
    7. if (err) return handleError(err);
    8. const story1 = new Story({
    9. title: 'Casino Royale',
    10. author: author._id // assign the _id from the person
    11. });
    12. story1.save(function (err) {
    13. if (err) return handleError(err);
    14. // that's it!
    15. });
    16. });

    So far we haven’t done anything much different. We’ve merely created a Person and a Story. Now let’s take a look at populating our story’s author using the query builder:

    1. Story.
    2. findOne({ title: 'Casino Royale' }).
    3. populate('author').
    4. exec(function (err, story) {
    5. if (err) return handleError(err);
    6. console.log('The author is %s', story.author.name);
    7. // prints "The author is Ian Fleming"
    8. });

    Populated paths are no longer set to their original _id , their value is replaced with the mongoose document returned from the database by performing a separate query before returning the results.

    Arrays of refs work the same way. Just call the populate method on the query and an array of documents will be returned in place of the original _ids.

    Setting Populated Fields

    You can manually populate a property by setting it to a document. The document must be an instance of the model your ref property refers to.

    1. Story.findOne({ title: 'Casino Royale' }, function(error, story) {
    2. if (error) {
    3. return handleError(error);
    4. }
    5. story.author = author;
    6. console.log(story.author.name); // prints "Ian Fleming"
    7. });

    Checking Whether a Field is Populated

    You can call the populated() function to check whether a field is populated. If populated() returns a , you can assume the field is populated.

    1. story.populated('author'); // truthy
    2. story.depopulate('author'); // Make `author` not populated anymore
    3. story.populated('author'); // undefined

    A common reason for checking whether a path is populated is getting the author id. However, for your convenience, Mongoose adds a _id getter to ObjectId instances so you can use story.author._id regardless of whether author is populated.

    1. story.populated('author'); // truthy
    2. story.author._id; // ObjectId
    3. story.depopulate('author'); // Make `author` not populated anymore
    4. story.populated('author'); // undefined
    5. story.author instanceof ObjectId; // true
    6. story.author._id; // ObjectId, because Mongoose adds a special getter

    What If There’s No Foreign Document?

    Mongoose populate doesn’t behave like conventional . When there’s no document, story.author will be null. This is analogous to a left join in SQL.

    1. await Person.deleteMany({ name: 'Ian Fleming' });
    2. const story = await Story.findOne({ title: 'Casino Royale' }).populate('author');
    3. story.author; // `null`

    If you have an array of authors in your storySchema, populate() will give you an empty array instead.

    1. const storySchema = Schema({
    2. authors: [{ type: Schema.Types.ObjectId, ref: 'Person' }],
    3. title: String
    4. });
    5. // Later
    6. const story = await Story.findOne({ title: 'Casino Royale' }).populate('authors');
    7. story.authors; // `[]`

    Field Selection

    What if we only want a few specific fields returned for the populated documents? This can be accomplished by passing the usual as the second argument to the populate method:

    1. Story.
    2. findOne({ title: /casino royale/i }).
    3. populate('author', 'name'). // only return the Persons name
    4. exec(function (err, story) {
    5. if (err) return handleError(err);
    6. console.log('The author is %s', story.author.name);
    7. // prints "The author is Ian Fleming"
    8. console.log('The authors age is %s', story.author.age);
    9. // prints "The authors age is null"
    10. });

    What if we wanted to populate multiple paths at the same time?

    1. Story.
    2. find(...).
    3. populate('fans').
    4. populate('author').
    5. exec();

    If you call populate() multiple times with the same path, only the last one will take effect.

    1. // The 2nd `populate()` call below overwrites the first because they
    2. // both populate 'fans'.
    3. Story.
    4. find().
    5. populate({ path: 'fans', select: 'name' }).
    6. populate({ path: 'fans', select: 'email' });
    7. // The above is equivalent to:
    8. Story.find().populate({ path: 'fans', select: 'email' });

    What if we wanted to populate our fans array based on their age and select just their names?

    1. Story.
    2. find().
    3. populate({
    4. path: 'fans',
    5. match: { age: { $gte: 21 } },
    6. // Explicitly exclude `_id`, see http://bit.ly/2aEfTdB
    7. select: 'name -_id'
    8. }).
    9. exec();

    The match option doesn’t filter out Story documents. If there are no documents that satisfy match, you’ll get a Story document with an empty fans array.

    1. findOne({ title: 'Casino Royale' }).
    2. populate({ path: 'author', name: { $ne: 'Ian Fleming' } }).
    3. exec();
    4. story.author; // `null`

    In general, there is no way to make populate() filter stories based on properties of the story’s author. For example, the below query won’t return any results, even though author is populated.

    1. const story = await Story.
    2. findOne({ 'author.name': 'Ian Fleming' }).
    3. populate('author').
    4. exec();
    5. story; // null

    If you want to filter stories by their author’s name, you should use denormalization.

    limit vs. perDocumentLimit

    Populate does support a limit option, however, it currently does not limit on a per-document basis for backwards compatibility. For example, suppose you have 2 stories:

    1. Story.create([
    2. { title: 'Casino Royale', fans: [1, 2, 3, 4, 5, 6, 7, 8] },
    3. { title: 'Live and Let Die', fans: [9, 10] }
    4. ]);

    If you were to populate() using the limit option, you would find that the 2nd story has 0 fans:

    That’s because, in order to avoid executing a separate query for each document, Mongoose instead queries for fans using numDocuments * limit as the limit. If you need the correct limit, you should use the perDocumentLimit option (new in Mongoose 5.9.0). Just keep in mind that will execute a separate query for each story, which may cause populate() to be slower.

    1. const stories = await Story.find().populate({
    2. path: 'fans',
    3. // Special option that tells Mongoose to execute a separate query
    4. // for each `story` to make sure we get 2 fans for each story.
    5. perDocumentLimit: 2
    6. });
    7. stories[0].name; // 'Casino Royale'
    8. stories[0].fans.length; // 2
    9. stories[1].name; // 'Live and Let Die'
    10. stories[1].fans.length; // 2

    Refs to children

    We may find however, if we use the author object, we are unable to get a list of the stories. This is because no story objects were ever ‘pushed’ onto author.stories.

    There are two perspectives here. First, you may want the author to know which stories are theirs. Usually, your schema should resolve one-to-many relationships by having a parent pointer in the ‘many’ side. But, if you have a good reason to want an array of child pointers, you can push() documents onto the array as shown below.

    1. author.stories.push(story1);
    2. author.save(callback);

    This allows us to perform a find and populate combo:

    1. Person.
    2. findOne({ name: 'Ian Fleming' }).
    3. populate('stories'). // only works if we pushed refs to children
    4. exec(function (err, person) {
    5. if (err) return handleError(err);
    6. console.log(person);
    7. });

    It is debatable that we really want two sets of pointers as they may get out of sync. Instead we could skip populating and directly find() the stories we are interested in.

    1. Story.
    2. find({ author: author._id }).
    3. exec(function (err, stories) {
    4. if (err) return handleError(err);
    5. console.log('The stories are an array: ', stories);
    6. });

    The documents returned from become fully functional, removeable, saveable documents unless the lean option is specified. Do not confuse them with . Take caution when calling its remove method because you’ll be removing it from the database, not just the array.

    If you have an existing mongoose document and want to populate some of its paths, you can use the Document#populate() method.

    1. const person = await Person.findOne({ name: 'Ian Fleming' });
    2. person.populated('stories'); // null
    3. // Call the `populate()` method on a document to populate a path.
    4. await person.populate('stories');
    5. person.populated('stories'); // Array of ObjectIds
    6. person.stories[0].name; // 'Casino Royale'

    The Document#populate() method does not support chaining. You need to call populate() multiple times, or with an array of paths, to populate multiple paths

    1. await person.populate(['stories', 'fans']);
    2. person.populated('fans'); // Array of ObjectIds

    Populating multiple existing documents

    If we have one or many mongoose documents or even plain objects (like output), we may populate them using the Model.populate() method. This is what Document#populate() and Query#populate() use to populate documents.

    Say you have a user schema which keeps track of the user’s friends.

    1. const userSchema = new Schema({
    2. name: String,
    3. friends: [{ type: ObjectId, ref: 'User' }]
    4. });

    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:

    1. User.
    2. findOne({ name: 'Val' }).
    3. populate({
    4. path: 'friends',
    5. // Get friends of friends - populate the 'friends' array for every friend
    6. populate: { path: 'friends' }
    7. });

    Cross Database Populate

    Let’s say you have a schema representing events, and a schema representing conversations. Each event has a corresponding conversation thread.

    1. const db1 = mongoose.createConnection('mongodb://localhost:27000/db1');
    2. const db2 = mongoose.createConnection('mongodb://localhost:27001/db2');
    3. const conversationSchema = new Schema({ numMessages: Number });
    4. const Conversation = db2.model('Conversation', conversationSchema);
    5. const eventSchema = new Schema({
    6. name: String,
    7. conversation: {
    8. type: ObjectId,
    9. ref: Conversation // `ref` is a **Model class**, not a string
    10. }
    11. });
    12. const Event = db1.model('Event', eventSchema);

    In the above example, events and conversations are stored in separate MongoDB databases. String ref will not work in this situation, because Mongoose assumes a string ref refers to a model name on the same connection. In the above example, the conversation model is registered on db2, not db1.

    1. // Works
    2. const events = await Event.
    3. find().
    4. populate('conversation');

    If you don’t have access to the model instance when defining your eventSchema, you can also pass .

    1. const events = await Event.
    2. find().
    3. // The `model` option specifies the model to use for populating.
    4. populate({ path: 'conversation', model: Conversation });

    Mongoose can also populate from multiple collections based on the value of a property in the document. Let’s say you’re building a schema for storing comments. A user may comment on either a blog post or a product.

    1. const commentSchema = new Schema({
    2. body: { type: String, required: true },
    3. on: {
    4. type: Schema.Types.ObjectId,
    5. required: true,
    6. // Instead of a hardcoded model name in `ref`, `refPath` means Mongoose
    7. // will look at the `onModel` property to find the right model.
    8. refPath: 'onModel'
    9. },
    10. onModel: {
    11. type: String,
    12. required: true,
    13. enum: ['BlogPost', 'Product']
    14. }
    15. });
    16. const Product = mongoose.model('Product', new Schema({ name: String }));
    17. const BlogPost = mongoose.model('BlogPost', new Schema({ title: String }));
    18. const Comment = mongoose.model('Comment', commentSchema);

    The refPath option is a more sophisticated alternative to ref. If ref is just a string, Mongoose will always query the same model to find the populated subdocs. With refPath, you can configure what model Mongoose uses for each document.

    1. const book = await Product.create({ name: 'The Count of Monte Cristo' });
    2. const post = await BlogPost.create({ title: 'Top 10 French Novels' });
    3. const commentOnBook = await Comment.create({
    4. body: 'Great read',
    5. on: book._id,
    6. onModel: 'Product'
    7. });
    8. const commentOnPost = await Comment.create({
    9. body: 'Very informative',
    10. on: post._id,
    11. onModel: 'BlogPost'
    12. });
    13. // The below `populate()` works even though one comment references the
    14. // 'Product' collection and the other references the 'BlogPost' collection.
    15. const comments = await Comment.find().populate('on').sort({ body: 1 });
    16. comments[0].on.name; // "The Count of Monte Cristo"
    17. comments[1].on.title; // "Top 10 French Novels"

    An alternative approach is to define separate blogPost and product properties on commentSchema, and then populate() on both properties.

    1. const commentSchema = new Schema({
    2. body: { type: String, required: true },
    3. product: {
    4. type: Schema.Types.ObjectId,
    5. required: true,
    6. ref: 'Product'
    7. },
    8. blogPost: {
    9. required: true,
    10. ref: 'BlogPost'
    11. }
    12. });
    13. // ...
    14. // The below `populate()` is equivalent to the `refPath` approach, you
    15. // just need to make sure you `populate()` both `product` and `blogPost`.
    16. populate('product').
    17. populate('blogPost').
    18. sort({ body: 1 });
    19. comments[0].product.name; // "The Count of Monte Cristo"
    20. comments[1].blogPost.title; // "Top 10 French Novels"

    Defining separate blogPost and product properties works for this simple example. But, if you decide to allow users to also comment on articles or other comments, you’ll need to add more properties to your schema. You’ll also need an extra populate() call for every property, unless you use mongoose-autopopulate. Using refPath means you only need 2 schema paths and one populate() call regardless of how many models your commentSchema can point to.

    Populate Virtuals

    So far you’ve only populated based on the _id field. However, that’s sometimes not the right choice. For example, suppose you have 2 models: Author and BlogPost.

    The above is an example of bad schema design. Why? Suppose you have an extremely prolific author that writes over 10k blog posts. That author document will be huge, over 12kb, and large documents lead to performance issues on both server and client. The states that one-to-many relationships, like author to blog post, should be stored on the “many” side. In other words, blog posts should store their author, authors should not store all their posts.

    1. const AuthorSchema = new Schema({
    2. name: String
    3. });
    4. const BlogPostSchema = new Schema({
    5. title: String,
    6. author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
    7. comments: [{
    8. author: { type: mongoose.Schema.Types.ObjectId, ref: 'Author' },
    9. content: String
    10. }]
    11. });

    Unfortunately, these two schemas, as written, don’t support populating an author’s list of blog posts. That’s where virtual populate comes in. Virtual populate means calling populate() on a virtual property that has a ref option as shown below.

    1. // Specifying a virtual with a `ref` property is how you enable virtual
    2. // population
    3. AuthorSchema.virtual('posts', {
    4. ref: 'BlogPost',
    5. localField: '_id',
    6. foreignField: 'author'
    7. });
    8. const Author = mongoose.model('Author', AuthorSchema, 'Author');
    9. const BlogPost = mongoose.model('BlogPost', BlogPostSchema, 'BlogPost');

    You can then populate() the author’s posts as shown below.

    1. const author = await Author.findOne().populate('posts');
    2. author.posts[0].title; // Title of the first blog post

    Keep in mind that virtuals are not included in toJSON() and toObject() output by default. If you want populate virtuals to show up when using functions like Express’ res.json() function or console.log(), set the virtuals: true option on your schema’s toJSON and toObject() options.

    1. const authorSchema = new Schema({ name: String }, {
    2. toJSON: { virtuals: true }, // So `res.json()` and other `JSON.stringify()` functions include virtuals
    3. toObject: { virtuals: true } // So `console.log()` and other functions that use `toObject()` include virtuals
    4. });

    If you’re using populate projections, make sure foreignField is included in the projection.

    1. let authors = await Author.
    2. find({}).
    3. // Won't work because the foreign field `author` is not selected
    4. populate({ path: 'posts', select: 'title' }).
    5. exec();
    6. authors = await Author.
    7. find({}).
    8. // Works, foreign field `band` is selected
    9. populate({ path: 'posts', select: 'title author' }).
    10. exec();

    Populate Virtuals: The Count Option

    Populate virtuals also support counting the number of documents with matching foreignField as opposed to the documents themselves. Set the count option on your virtual:

    1. const PersonSchema = new Schema({
    2. name: String,
    3. band: String
    4. });
    5. const BandSchema = new Schema({
    6. name: String
    7. });
    8. BandSchema.virtual('numMembers', {
    9. ref: 'Person', // The model to use
    10. localField: 'name', // Find people where `localField`
    11. foreignField: 'band', // is equal to `foreignField`
    12. count: true // And only get the number of docs
    13. });
    14. // Later
    15. const doc = await Band.findOne({ name: 'Motley Crue' }).
    16. populate('numMembers');
    17. doc.numMembers; // 2

    Populating Maps

    are a type that represents an object with arbitrary string keys. For example, in the below schema, members is a map from strings to ObjectIds.

    1. const BandSchema = new Schema({
    2. name: String,
    3. members: {
    4. type: Map,
    5. of: {
    6. type: 'ObjectId',
    7. ref: 'Person'
    8. }
    9. }
    10. });
    11. const Band = mongoose.model('Band', bandSchema);

    This map has a ref, which means you can use populate() to populate all the ObjectIds in the map. Suppose you have the below band document:

    1. const person1 = new Person({ name: 'Vince Neil' });
    2. const person2 = new Person({ name: 'Mick Mars' });
    3. const band = new Band({
    4. name: 'Motley Crue',
    5. members: {
    6. 'singer': person1._id,
    7. 'guitarist': person2._id
    8. }
    9. });

    You can populate() every element in the map by populating the special path members.$*. $* is a special syntax that tells Mongoose to look at every key in the map.

    1. const band = await Band.findOne({ name: 'Motley Crue' }).populate('members.$*');
    2. band.members.get('singer'); // { _id: ..., name: 'Vince Neil' }

    You can also populate paths in maps of subdocuments using $*. For example, suppose you have the below librarySchema:

    1. const librarySchema = new Schema({
    2. name: String,
    3. books: {
    4. type: Map,
    5. of: new Schema({
    6. title: String,
    7. author: {
    8. type: 'ObjectId',
    9. ref: 'Person'
    10. }
    11. })
    12. }
    13. });
    14. const Library = mongoose.model('Library, librarySchema');

    You can populate() every book’s author by populating books.$*.author:

    1. const libraries = await Library.find().populate('books.$*.author');
    1. // Always attach `populate()` to `find()` calls
    2. MySchema.pre('find', function() {
    3. this.populate('user');
    4. });
    1. // Always `populate()` after `find()` calls. Useful if you want to selectively populate
    2. // based on the docs found.
    3. MySchema.post('find', async function(docs) {
    4. for (let doc of docs) {
    5. if (doc.isPublic) {
    6. await doc.populate('user');
    7. }
    8. }
    9. });
    1. // `populate()` after saving. Useful for sending populated data back to the client in an
    2. // update API endpoint
    3. MySchema.post('save', function(doc, next) {
    4. doc.populate('user').then(function() {
    5. next();

    Now that we’ve covered populate(), let’s take a look at discriminators.