Nested Populate
🎯 Function Overview
Nested populate lets you populate relations on documents that were already populated, so you can load a multi-level document graph with one chain.
Model schema examples on this page use the runtime-scoped s namespace passed by monSQLize. Application code does not need to import the root schema-dsl entry for these examples.
📖 How to use
1. Basic nested populate
Populate the posts association and then further populate the posts.comments association:
const User = msq.model('users');
const result = await User.findOne({ _id: userId })
.populate({
path: 'posts',
populate: 'comments' // nested populate
});
//Result structure:
// {
// _id: userId,
// username: 'john',
// posts: [
// {
// _id: postId,
// title: 'My Post',
// comments: [ // nested populated data
// { _id: commentId, content: 'Great!' }
// ]
// }
// ]
// }
2. Nested populate object configuration
Nested populate also supports full configuration options:
const result = await User.findOne({ _id: userId })
.populate({
path: 'posts',
populate: {
path: 'comments',
select: 'content', // select only specific fields
sort: { createdAt: -1 },
limit: 10
}
});
3. Multi-level nested populate
Supports 3 or more levels of nesting:
// User -> Post -> Comment -> Author
const result = await User.findOne({ _id: userId })
.populate({
path: 'posts',
populate: {
path: 'comments',
populate: 'author' // third level
}
});
//Result structure:
// {
// username: 'john',
// posts: [
// {
// title: 'My Post',
// comments: [
// {
// content: 'Great!',
// author: { // third-level populated data
// username: 'jane'
// }
// }
// ]
// }
// ]
// }
4. Nesting multiple populates
Multiple associations can be populated simultaneously at a nested level:
const result = await User.findOne({ _id: userId })
.populate({
path: 'posts',
populate: ['comments', 'likes'] // populate multiple relations
});
//Result structure:
// {
// posts: [
// {
// title: 'My Post',
// comments: [...],
// likes: [...]
// }
// ]
// }
5. Mix chained and nested populates
You can use both chained and nested populates:
const result = await User.findOne({ _id: userId })
.populate('profile') // chained populate
.populate({
path: 'posts',
populate: 'comments' // nested populate
});
//Result structure:
// {
// profile: { bio: '...' },
// posts: [
// {
// comments: [...]
// }
// ]
// }
📋 Complete example
Model Definition
import { Model } from 'monsqlize';
// User Model
Model.define('users', {
schema: (s) => s({
username: 'string!',
profileId: 'objectId'
}),
relations: {
profile: {
from: 'profiles',
localField: 'profileId',
foreignField: '_id',
single: true
},
posts: {
from: 'posts',
localField: '_id',
foreignField: 'authorId',
single: false
}
}
});
// Post Model
Model.define('posts', {
schema: (s) => s({
title: 'string!',
authorId: 'objectId'
}),
relations: {
comments: {
from: 'comments',
localField: '_id',
foreignField: 'postId',
single: false
},
likes: {
from: 'likes',
localField: '_id',
foreignField: 'postId',
single: false
}
}
});
// Comment Model
Model.define('comments', {
schema: (s) => s({
content: 'string!',
postId: 'objectId',
authorId: 'objectId'
}),
relations: {
author: {
from: 'users',
localField: 'authorId',
foreignField: '_id',
single: true
}
}
});
Query example
//Example 1: Basic nesting
const user1 = await User.findOne({ username: 'john' })
.populate({
path: 'posts',
populate: 'comments'
});
//Example 2: Multiple levels of nesting
const user2 = await User.findOne({ username: 'john' })
.populate({
path: 'posts',
populate: {
path: 'comments',
populate: 'author' // comment author
}
});
//Example 3: Nesting multiple associations
const user3 = await User.findOne({ username: 'john' })
.populate({
path: 'posts',
populate: ['comments', 'likes']
});
//Example 4: Mixed use
const user4 = await User.findOne({ username: 'john' })
.populate('profile')
.populate({
path: 'posts',
populate: {
path: 'comments',
select: 'content',
sort: { createdAt: -1 },
limit: 5
}
});
Runtime behavior
- First-level populate can read from the collection named in
from.
- If
from matches a registered Model, monSQLize hydrates the related documents through that Model.
- Nested populate continues only when the related collection has a registered Model, because the next relation set must come from that Model definition.
- The runtime applies
select, sort, skip, and limit after the related documents are loaded.
- Nested populate has depth and cycle guards. If a nested path is invalid, the query fails with a user-facing argument error.
⚠️ Notes
1. Define Models for nested branches
First-level populate can load plain related documents from a collection. Nested populate needs a Model definition for the related collection, otherwise monSQLize has no relation metadata for the next hop:
// comments collection has no defined Model
Model.define('posts', {
relations: {
comments: { from: 'comments', ... }
}
});
// comments are loaded at the first level only; the nested branch has no relation metadata
await User.findOne().populate({
path: 'posts',
populate: 'comments'
});
Nested populate executes multiple database queries. Keep the relation graph shallow and index foreign keys:
//Performance analysis:
//User.find() → 1 query (10 users)
//.populate('posts') → 1 query (query all users' posts)
// .populate({ path: 'posts', populate: 'comments' })
//→ Query again (query the comments of all posts)
//Total: 3 queries
Optimization suggestions:
- Use
select to select only necessary fields
- Use
limit to limit the amount of associated data
- Avoid deep relation graphs unless the user path really needs them
3. Circular reference
Avoid circular references leading to infinite recursion:
// Risk: User -> Post -> Author(User) -> Post -> ...
Model.define('users', {
relations: {
posts: { from: 'posts', ... }
}
});
Model.define('posts', {
relations: {
author: { from: 'users', ... }
}
});
// This creates a loop:
await User.find().populate({
path: 'posts',
populate: {
path: 'author',
populate: 'posts' //← Loop back to posts
}
});
Solution: design nested paths deliberately, set explicit limits, and avoid bidirectional nested chains.
📊 Compatibility
🧪 Test cases
For complete test cases, please refer to:
test/integration/model/model-features.test.ts
test/integration/model/model-schema-and-hooks.test.ts
Coverage includes:
- Basic nested populate
- Nested populate object configuration
- Three-level nesting
- Multiple nested populate paths
- Mixed chained and nested populate