Function: Model layer related query, supports 6 methods ()
📖 Overview
Populate is the Model-layer helper for loading related documents declared in relations.
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.
Core Features
- ✅ 6 methods supported - find/findOne/findByIds/findOneById/findAndCount/findPage ()
- ✅ Chain API - Supports multiple populate chain calls
- ✅ Smart Cache - Related query results can use the configured cache; impact depends on hit rate, backend, and workload
- ✅ Field Selection - Only the required fields are returned, reducing data transmission
- ✅ Sort Limit - Support sort and limit to control related data
- ✅ Automatic Injection - Results are automatically injected into the document object
🚀 Quick Start
1. Define relationships
First configure relations in the Model definition:
import { Model } from 'monsqlize';
//Define User Model
Model.define('users', {
schema: (s) => s({
username: 'string!',
email: 'email!'
}),
relations: {
//hasMany: one-to-many
posts: {
from: 'posts',
localField: '_id',
foreignField: 'userId',
single: false
},
//hasOne: one to one
profile: {
from: 'profiles',
localField: '_id',
foreignField: 'userId',
single: true
}
}
});
//Define Post Model
Model.define('posts', {
schema: (s) => s({
title: 'string!',
content: 'string!'
}),
relations: {
//belongsTo: many to one
author: {
from: 'users',
localField: 'userId',
foreignField: '_id',
single: true
}
}
});
2. Use populate
const msq = new MonSQLize({ ... });
await msq.connect();
const User = msq.model('users');
//basic populate
const users = await User.find({ age: { $gte: 18 } })
.populate('posts');
console.log(users[0].posts); // [{ title: '...', content: '...' }, ...]
3. Multiple populate
//Chain multiple populate calls
const users = await User.find()
.populate('posts')
.populate('profile');
// users[0].posts = [...]
// users[0].profile = { avatar: '...', bio: '...' }
4. Option configuration
//populate with options
const users = await User.find()
.populate('posts', {
select: ['title', 'createdAt'], //Return only these fields
limit: 10, //Up to 10 items
sort: { createdAt: -1 } //In descending order of creation time
})
.populate('profile', {
select: ['avatar', 'bio']
});
📚Supported query methods
monSQLize supports 6 query methods of populate, leading the industry:
Example
// find + populate
const users = await User.find().populate('posts');
// findOne + populate
const user = await User.findOne({ username: 'john' }).populate('profile');
// findByIds + populate
const users = await User.findByIds([id1, id2, id3]).populate('posts');
// findOneById + populate
const user = await User.findOneById(userId).populate('posts');
// findAndCount + populate
const result = await User.findAndCount({ age: { $gte: 18 } }).populate('posts');
// result.data[0].posts = [...]
// findPage + populate
const result = await User.findPage({ limit: 10 }).populate('posts');
// result.items[0].posts = [...]
🎯 API Reference
.populate(path, options?)
Add related queries to the query chain.
Parameters:
path (String) - relationship path, must be defined in relations of Model
options (Object, optional) - Populate option
Returns: PopulateProxy object (supports chained calls and Promise)
Options:
Example:
//Full options
const users = await User.find()
.populate('posts', {
select: ['title', 'createdAt'],
limit: 5,
sort: { createdAt: -1 },
cache: 60000
});
💡 Usage scenarios
Scenario 1: User-Article (hasMany)
//definition
Model.define('users', {
schema: (s) => s({ username: 'string!' }),
relations: {
posts: {
from: 'posts',
localField: '_id',
foreignField: 'userId',
single: false // hasMany
}
}
});
//use
const users = await User.find()
.populate('posts', { limit: 10 });
users.forEach(user => {
console.log(`${user.username} has ${user.posts.length} posts`);
});
Scenario 2: Article-Author (belongsTo)
//definition
Model.define('posts', {
schema: (s) => s({ title: 'string!' }),
relations: {
author: {
from: 'users',
localField: 'userId',
foreignField: '_id',
single: true // belongsTo (hasOne)
}
}
});
//use
const posts = await Post.find()
.populate('author', { select: ['username', 'avatar'] });
posts.forEach(post => {
console.log(`${post.title} Author: ${post.author.username}`);
});
Scenario 3: User-Profile (hasOne)
//definition
Model.define('users', {
schema: (s) => s({ username: 'string!' }),
relations: {
profile: {
from: 'profiles',
localField: '_id',
foreignField: 'userId',
single: true // hasOne
}
}
});
//use
const user = await User.findOne({ username: 'john' })
.populate('profile');
console.log(user.profile.bio);
//Define students
Model.define('students', {
schema: (s) => s({ name: 'string!' }),
relations: {
enrollments: {
from: 'student_course',
localField: '_id',
foreignField: 'studentId',
single: false
}
}
});
//Define intermediate table
Model.define('student_course', {
schema: (s) => s({
studentId: 'objectId!',
courseId: 'objectId!'
}),
relations: {
course: {
from: 'courses',
localField: 'courseId',
foreignField: '_id',
single: true
}
}
});
//Use (requires populate twice)
const students = await Student.find()
.populate('enrollments');
// For deeper paths, use nested populate configuration.
for (const student of students) {
const courseIds = student.enrollments.map(e => e.courseId);
const courses = await Course.findByIds(courseIds);
student.courses = courses;
}
N+1 problem solving
❌ Traditional method (N+1 problem):
//Query users: 1 time
const users = await User.find();
//Query articles for each user: N times
for (const user of users) {
user.posts = await Post.find({ userId: user._id });
}
//Total number of queries: 1 + N times (100 users = 101 queries)
✅ monSQLize Populate (batch query):
//Only 2 queries are needed
const users = await User.find().populate('posts');
//Time 1: Query users
//The 2nd time: Batch query for all users’ articles (WHERE userId IN [...])
//Total queries for this single-level relation: 2, rather than one related query per user.
Smart caching
Populate + caching:
//First query
const users = await User.find({}, { cache: 60000 }).populate('posts');
//2 database queries (user + article)
//Subsequent query (within 60 seconds)
const users2 = await User.find({}, { cache: 60000 }).populate('posts');
//No database query when every required entry is a valid cache hit.
Performance characteristics:
Query-count reduction is structural; latency is workload-dependent. Measure it using the performance evidence contract.
Caching strategy
//The main query and populate use the same cache time
const users = await User.find({}, { cache: 60000 }).populate('posts');
//Main query and populate use different cache times
const users = await User.find({}, { cache: 60000 })
.populate('posts', { cache: 300000 }); //Article cache for 5 minutes
//Only cache populate, not the main query
const users = await User.find()
.populate('posts', { cache: 60000 });
🆚Comparison with Mongoose
Support method comparison
// Mongoose
const users = await User.find().populate('posts');
const users2 = await User.find().populate('posts'); // Queries again unless the application adds caching.
// monSQLize
const users = await User.find({}, { cache: 60000 }).populate('posts'); // Cold path for this key.
const users2 = await User.find({}, { cache: 60000 }).populate('posts'); // Potential warm-cache path.
Record cold and warm distributions for the same dataset rather than inferring a multiplier from this control-flow example; see Performance evidence.
API comparison
// Mongoose
User.find()
.populate('posts')
.populate({ path: 'profile', select: 'avatar bio' });
//monSQLize (more concise)
User.find()
.populate('posts')
.populate('profile', { select: ['avatar', 'bio'] });
❓ FAQ
Q1: Can it be cached after populate?
A: ✅ Yes! This is a unique advantage of monSQLize.
//populate results will be cached
const users = await User.find({}, { cache: 60000 }).populate('posts');
//Subsequent queries read directly from the cache, including populate data
const users2 = await User.find({}, { cache: 60000 }).populate('posts');
//0 database queries ⚡
Q2: How to avoid the N+1 problem?
A: monSQLize automatically solves the N+1 problem.
Populate batches relation lookups:
// Automatically converted to WHERE userId IN [id1, id2, id3, ...]
const users = await User.find().populate('posts');
// Only 2 queries are required (user + batch query articles)
Q3: Can populate be nested?
A: The current version supports nested populate, and the next-level relationship can be declared directly in the populate configuration.
const students = await Student.find()
.populate({ path: 'enrollments', populate: 'course' });
Nested populate is capped by maxDepth (default 5) to protect self-referencing relations from unbounded recursion:
const categories = await Category.find()
.populate({ path: 'children', populate: 'children', maxDepth: 3 });
A: No, on the contrary, the performance is better.
- ✅ Batch query (avoid N+1)
- ✅ Smart caching (measure hit rate, backend latency, and invalidation cost for the target workload)
- ✅ Field selection (reduce data transfer)
Q5: Is it necessary to populate after the relationship is defined?
A: No, populate is optional.
//No populate, only query users
const users = await User.find();
// users[0].posts = undefined
//There is related data only after populate
const users = await User.find().populate('posts');
// users[0].posts = [...]
Q6: Does populate support conditional filtering?
A: Supported. Use options such as match, skip, and limit to constrain the populated data. For has-many relations, skip and limit are applied per parent document after related records are grouped.
const users = await User.find()
.populate('posts', {
match: { status: 'published' }, //Populate only published articles
limit: 10
});
📝 Complete example
Blog system example
import MonSQLize from 'monsqlize';
const { Model } = MonSQLize;
//1. Define Models
Model.define('users', {
schema: (s) => s({
username: 'string:3-32!',
email: 'email!',
avatar: 'url'
}),
relations: {
posts: {
from: 'posts',
localField: '_id',
foreignField: 'authorId',
single: false
},
profile: {
from: 'profiles',
localField: '_id',
foreignField: 'userId',
single: true
}
}
});
Model.define('posts', {
schema: (s) => s({
title: 'string!',
content: 'string!',
status: 'string'
}),
relations: {
author: {
from: 'users',
localField: 'authorId',
foreignField: '_id',
single: true
},
comments: {
from: 'comments',
localField: '_id',
foreignField: 'postId',
single: false
}
}
});
Model.define('comments', {
schema: (s) => s({
content: 'string!',
postId: 'objectId!',
userId: 'objectId!'
}),
relations: {
user: {
from: 'users',
localField: 'userId',
foreignField: '_id',
single: true
}
}
});
Model.define('profiles', {
schema: (s) => s({
bio: 'string',
website: 'url',
userId: 'objectId!'
})
});
//2. Connect to the database
const msq = new MonSQLize({
type: 'mongodb',
config: { uri: 'mongodb://localhost:27017/blog' },
cache: { ttl: 60000, max: 1000 } //Enable caching
});
await msq.connect();
const User = msq.model('users');
const Post = msq.model('posts');
const Comment = msq.model('comments');
//3. Usage scenarios
//Scenario A: Get users and their articles
const users = await User.find({}, { cache: 60000 })
.populate('posts', {
select: ['title', 'createdAt', 'status'],
limit: 5,
sort: { createdAt: -1 }
})
.populate('profile');
console.log('User list:');
users.forEach(user => {
console.log(`- ${user.username}`);
console.log(`Profile: ${user.profile?.bio || 'No profile'}`);
console.log(`Number of posts: ${user.posts.length}`);
user.posts.forEach(post => {
console.log(` - ${post.title}`);
});
});
//Scenario B: Get articles, authors and comments
const posts = await Post.find(
{ status: 'published' },
{ cache: 60000 }
).populate('author', {
select: ['username', 'avatar']
}).populate('comments', {
limit: 10
});
console.log('\nArticle list:');
posts.forEach(post => {
console.log(`- ${post.title}`);
console.log(`Author: ${post.author.username}`);
console.log(`Number of comments: ${post.comments.length}`);
});
//Scenario C: Query users by page
const result = await User.findPage({
limit: 10,
page: 1
}).populate('posts', { limit: 3 });
console.log(`\nTotal number of users: ${result.pageInfo.totalCount}`);
console.log(`Current page: ${result.pageInfo.currentPage}/${result.pageInfo.totalPages}`);
result.items.forEach(user => {
console.log(`- ${user.username}: ${user.posts.length} latest posts`);
});
📌 Best Practices
1. Reasonable use of field selection
//❌ Bad: returns all fields (wasted bandwidth)
const users = await User.find().populate('posts');
//✅ Good: only return required fields
const users = await User.find().populate('posts', {
select: ['title', 'createdAt']
});
2. Limit the amount of associated data
//❌ Bad: May return thousands of reviews
const posts = await Post.find().populate('comments');
//✅ Good: limited quantity
const posts = await Post.find().populate('comments', {
limit: 10,
sort: { createdAt: -1 }
});
3. Enable smart caching
//❌ Bad: Query the database every time
const users = await User.find().populate('posts');
//✅ Good: Enable caching (suitable for reading more data and writing less data)
const users = await User.find({}, { cache: 60000 }).populate('posts');
4. Populate on demand
//❌ Bad: always populate (even if not needed)
const users = await User.find()
.populate('posts')
.populate('profile')
.populate('comments');
//✅ Good: only populate required relationships
const users = await User.find()
.populate('profile'); //Just need information