Introduction:
In MongoDB, especially when working with Mongoose in Node.js, there are two main ways to fetch related data:
- Using MongoDB's native
$lookup
in aggregation pipelines - Using Mongoose’s built-in
ref
andpopulate()
functionality
While $lookup
gives fine-grained control and works directly at the database level, Mongoose's ref-based population simplifies your code, makes your data models more maintainable, and leads to better developer experience for most use cases.
Let’s explore this using a real-world project management example involving Sprints and Tasks.
Schema Design: Tasks and Sprints
Here's how we model the relationship between Tasks and Sprints in Mongoose using ref
and populate
.
const taskSchema = new mongoose.Schema({
sprintId: {
type: mongoose.Types.ObjectId,
ref: 'sprints'
},
projectId: {
type: mongoose.Types.ObjectId,
ref: 'projects',
required: true
},
title: { type: String, required: true, minlength: 3 },
description: { type: String, default: '' },
type: {
type: String,
enum: ['Story', 'Bug', 'Task', 'Epic', 'Sub-task'],
default: 'Task'
},
status: {
type: String,
enum: ['To Do', 'In Progress', 'In Review', 'Done', 'Blocked'],
default: 'To Do'
},
priority: {
type: String,
enum: ['Low', 'Medium', 'High', 'Critical'],
default: 'Medium'
},
assignee: {
_id: mongoose.Types.ObjectId,
email: String,
displayName: String
},
reporter: {
_id: mongoose.Types.ObjectId,
email: String,
displayName: String
},
estimate: { type: Number, default: 0 },
createdBy: {
_id: mongoose.Types.ObjectId,
email: String,
displayName: String
},
createdAt: { type: Date, default: Date.now },
dueDate: Date,
lastModified: Date
});
Task Creation Logic
When creating a task, we also push its ID into the corresponding Sprint:
const taskModel = {
createTask: async (sprintId, projectId, title, description, type, status, priority, assignee, reporter, estimate, createdBy, createdAt, dueDate, lastModified) => {
const task = new Task({
sprintId: sprintId ? new mongoose.Types.ObjectId(sprintId) : undefined,
projectId: new mongoose.Types.ObjectId(projectId),
title,
description,
type,
status,
priority,
assignee,
reporter,
estimate,
createdBy,
createdAt,
dueDate,
lastModified
});
const savedTask = await task.save();
if (sprintId) {
const Sprint = mongoose.model('sprints');
await Sprint.findByIdAndUpdate(
sprintId,
{ $push: { tasks: task._id } },
{ new: true }
);
}
return savedTask;
}
};
Sprint Schema with tasks
as Array of ref
const sprintSchema = new mongoose.Schema({
projectId: {
type: mongoose.Types.ObjectId,
ref: 'projects',
required: true
},
sprintNum: { type: String, required: true },
name: { type: String, required: true },
description: { type: String, default: '' },
duration: { type: String, enum: ['1 Week', '2 Weeks', '3 Weeks'] },
startDate: { type: Date, required: true },
endDate: { type: Date, required: true },
tasks: [{
type: mongoose.Types.ObjectId,
ref: 'tasks'
}],
createdBy: {
_id: mongoose.Types.ObjectId,
email: String,
displayName: String
}
}, { timestamps: true });
Populating Tasks from Sprints
Instead of writing a complex $lookup
query, we simply do:
const getAllSprints = async () => {
return await Sprint.find({}).populate('tasks').exec();
};
This automatically pulls in all related task documents for each sprint!
Some plus factors to use ref over lookup
- The code is very readable compared to the lookup.
- The integration of mongoose is seamless. Only thing you just have too push the ids into the array and ref and populate will do the job automatically.
- Note: This will be easier for small one-level joins. If you have multi-level joins or complex joins then aggregate is the best to do so.
Conclusion
If you're using Mongoose, stop reaching for $lookup unless you have a really complex use case. For most apps, especially CRUD-based systems like task managers, ref and populate() are more than enough.
Not only is the syntax cleaner, but it also keeps your code more expressive and schema-driven. Let MongoDB handle relationships behind the scenes—just like a good ORM should.
Top comments (1)
Nice, stuff like this makes me wanna clean up half my own code tbh