How to "deep populate" using MongoDB and Mongoose?
August 08, 2019
One-layer population
If you found this blog post, you most likely already know what populating a field in a MongoDB document means, you are just wondering how to populate a field in a populated document.
In any case, here is a small example of regular population:
person.js - schema for persons
const mongoose = require('mongoose')
const personSchema = mongoose.Schema({
name: {
type: String,
required: true,
},
address: {
mongoose.Schema.Types.ObjectId,
ref: 'Address',
}
module.exports = mongoose.model('Person', personSchema)
address.js - schema for addresses
const mongoose = require('mongoose')
const addressSchema = mongoose.Schema({
street: {
type: String,
required: true,
},
zipCode: {
type: String,
required: true,
},
city: {
type: String,
required: true,
},
country: {
type: String,
required: true,
},
module.exports = mongoose.model('Address', addressSchema)
So we have two schemas, one for person and one for address. The person schema refers to the address schema. That means that the field address
in personSchema
contains the _id
of an address document. With that _id
, we can join the two documents together in SQL terms.
This is how it is done:
const person = await Person.findOne({
name: "John",
}).populate("address")
We don’t have to populate all of the fields of an address document. We can select just the ones we need.
This is how it is done:
const person = await Person.findOne({
name: "John",
}).populate("address", { street: 1, zipCode: 1 })
That would return a person
document with only street
and zipCode
in the address field. I should mention that the _id
field of the address
document will always be there. You don’t have to explicitly select it.
Two-layer population
What if you needed to populate a field that is in a document that you need to populate? I know, that phrase is a bit hard to grasp. I try again. Take a look at the address
schema. What if country
field was a reference to a country
document:
const addressSchema = mongoose.Schema({
street: {
type: String,
required: true,
},
zipCode: {
type: String,
required: true,
},
city: {
type: String,
required: true,
},
country: {
mongoose.Schema.Types.ObjectId,
ref: 'Country',
},
module.exports = mongoose.model('Address', addressSchema)
Let’s create a schema for the country as well:
const mongoose = require('mongoose')
const countrySchema = mongoose.Schema({
name: {
type: String,
required: true,
},
population: {
type: Number,
required: true,
},
module.exports = mongoose.model('Country', personSchema)
In the first example we had to populate address
field with an address
document. If we populate only once, and we need also details about the country
, we have to populate more than one layer deep.
If you have encountered this problem, the first thing you try is probably chaining two populate
methods like this:
const person = await Person.findOne({
name: "John",
})
.populate("address")
.populate("country")
This doesn’t work and once you think about it you notice that it shouldn’t work either. The second populate
call is trying to populate a field called country
in the person
schema, not in the address
schema.
In order to populate the second layer, the syntax changes. First let’s take a look at an example for one-layer population with the other syntax:
const person = await Person.findOne({ name: "John" }).populate([
{
path: "address",
model: "Address",
select: "street zipCode",
},
])
path
refers to the field to be populated. If there were more fields to populate, we could put the here in the same string separated by spaces.model
refers to the model/schema to be used for population.select
let’s us select the fields we want to populate.
That is the equivalent of:
const person = await Person.findOne({
name: "John",
}).populate("address", { street: 1, zipCode: 1 })
Now, let’s populate also the country
field:
const person = await Person.findOne({ name: "John" }).populate([
{
path: 'address',
model: 'Address',
select: 'street zipCode'
populate: {
path: 'country',
model: 'Country',
}
},
])
We added a new property populate
to the population options. The property as its value has a new object with path
and model
properties. If there was yet another field to populate, we could use populate
property even nested there. So we are not limited to one-layer or two-layer population.
There’s more
You may have noticed that the populate
method consumes an array as an argument. It is not an array just for laughs. It is an array, because we may need to populate more fields in the same document using different models.
You could do something like this:
const person = await Person.findOne({ name: "John" }).populate([
{
path: "address",
model: "Address",
select: "street zipCode",
},
{
path: "friends",
model: "Person",
select: "age",
},
])
That populate
call would populate also a hypothetical friends
field with the _id
and age
fields of the persons that are listed as friends of John.
Conclusion
I think that this blog post covers pretty well the cases of field population using mongoose
. I didn’t include populating aggregates, because I wanted to keep this blog post as concise as possible, so that it would be easier to reference to when needed.