Database Relationships and References
Data Relationships in MongoDB
Real applications have data that connects together. A user has many orders. An order belongs to one user and contains many products. A student can enrol in many courses, and each course can have many students. These connections are called relationships, and modelling them correctly is one of the most important skills in backend development.
MongoDB gives you two strategies for handling relationships: embedding documents inside each other, or referencing them with IDs. Choosing the right approach depends on how you read and write the data.
Types of Relationships
One-to-One
One entity is associated with exactly one other entity.
Example: A user has one profile. A company has one registered address.
One-to-Many
One entity is associated with many others, but each child belongs to only one parent.
Example: One author writes many blog posts. One order contains many items.
Many-to-Many
Many entities on both sides are connected to many on the other.
Example: Many students enrol in many courses. Many products belong to many categories.
Strategy 1: Embedding Documents
Embedding means storing related data directly inside a parent document.
// Order document with embedded items
{
"_id": "64abc001",
"customerId": "64user001",
"status": "shipped",
"items": [
{ "productName": "Wireless Headphones", "quantity": 1, "priceKobo": 1500000 },
{ "productName": "Phone Case", "quantity": 2, "priceKobo": 150000 }
],
"totalKobo": 1800000,
"createdAt": "2024-09-01T10:00:00Z"
}
The order items are embedded directly. To display an order with all its items, you fetch one document — no joins required.
When to embed:
- The child data is always accessed with the parent (you always show items with the order)
- The child data does not need to exist independently
- The array has a bounded, predictable size (avoid embedding arrays that could grow to thousands of items)
Strategy 2: Referencing Documents
Referencing means storing the _id of a related document rather than the full data.
// Post document references the author by ID
{
"_id": "64post001",
"title": "Getting Started with MongoDB",
"body": "MongoDB is a document database...",
"author": "64user001", // References the User document
"tags": ["mongodb", "database"],
"createdAt": "2024-09-05T08:00:00Z"
}
// User document (separate collection)
{
"_id": "64user001",
"name": "Ngozi Okonkwo",
"email": "ngozi@example.com"
}
When to reference:
- The related data is accessed independently as well as together
- The same data is referenced by many documents (a product is in many orders)
- The child data is large and not always needed with the parent
- The relationship is many-to-many
Defining References in Mongoose
Use mongoose.Schema.Types.ObjectId and the ref property to define a reference:
const postSchema = new mongoose.Schema({
title: { type: String, required: true },
body: { type: String, required: true },
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // The name of the model that populate() will look up
required: true,
},
tags: [String],
}, { timestamps: true });
const Post = mongoose.model('Post', postSchema);
When you create a post, you store the author's _id:
const post = await Post.create({
title: 'My First Post',
body: 'This is the content...',
author: req.user._id, // The logged-in user's ID
});
populate() — Joining Related Documents
When you retrieve a post with just an author ID, you cannot display the author's name without fetching the User document too. Mongoose's populate() method does this automatically:
// Without populate — author is just an ID string
const post = await Post.findById(postId);
// { title: "...", author: "64user001", ... }
// With populate — author is replaced with the full User document
const post = await Post.findById(postId).populate('author');
// { title: "...", author: { name: "Ngozi", email: "ngozi@..." }, ... }
Selecting Specific Fields When Populating
You rarely need every field of the referenced document:
// Only get the author's name and email — exclude password and other sensitive fields
const post = await Post.findById(postId)
.populate('author', 'name email -_id');
Populating Multiple References
const commentSchema = new mongoose.Schema({
body: String,
post: { type: mongoose.Schema.Types.ObjectId, ref: 'Post' },
author: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
});
// Populate both references in one query
const comment = await Comment.findById(commentId)
.populate('post', 'title')
.populate('author', 'name');
Modelling Many-to-Many Relationships
For many-to-many relationships, store an array of references on one or both sides.
// Course enrolment: a student has many courses, a course has many students
const courseSchema = new mongoose.Schema({
title: String,
instructor: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
students: [{ type: mongoose.Schema.Types.ObjectId, ref: 'User' }],
});
// Enrol a student
await Course.updateOne(
{ _id: courseId },
{ $addToSet: { students: studentId } } // $addToSet prevents duplicate enrolments
);
// Get a course with all enrolled students
const course = await Course.findById(courseId)
.populate('students', 'name email')
.populate('instructor', 'name');
Embedding vs Referencing: Decision Guide
| Question | Embed | Reference |
|---|---|---|
| Is child data always shown with parent? | Yes | No |
| Does child data stand alone? | No | Yes |
| Is the array unbounded (could grow forever)? | No | Yes |
| Is data shared across many documents? | No | Yes |
| Do you need to update child data in one place? | No | Yes |
A practical example: store a user's address embedded (it is always shown with the user, unique to them), but store their orders as separate documents referenced by user ID (orders grow over time and need to be queried independently).
Practice Exercise
Model a blog platform with proper relationships:
- Create a
Usermodel with name, email, and password fields - Create a
Postmodel that referencesUserasauthor, and embedscommentsas an array of objects (withbody,authorName, andcreatedAt) — keep comments embedded since they are always shown with the post - Create a
Categorymodel, then updatePostto have acategoriesfield that references multipleCategorydocuments (many-to-many) - Write a route
GET /poststhat returns posts withauthorpopulated (name and email only) - Write a route
GET /posts/:idthat populates bothauthorandcategories - Test using Postman: create a user, create categories, create posts that reference them, then verify the populated responses
Try it yourself
Key Takeaways
- MongoDB handles relationships through two strategies: embedding documents inside each other, or referencing them with ObjectId fields.
- Embed when child data is always accessed with the parent, belongs exclusively to it, and has a bounded size.
- Reference when child data is accessed independently, shared across many documents, or could grow without bound.
- Mongoose's populate() method replaces ObjectId references with the full referenced documents, similar to a SQL JOIN.
- For many-to-many relationships, store an array of ObjectIds on one or both sides, and use $addToSet to prevent duplicate references.
Quick Quiz
1.What does Mongoose's populate() method do?
2.When should you prefer embedding a document rather than referencing it?
3.Which Mongoose operator should you use when enrolling a student in a course to prevent duplicate enrolments?
4.In a Mongoose schema, what does the 'ref' property in an ObjectId field specify?
Ready to go further?
CareerEx gives you structured 12-week training, live classes every Saturday and Sunday, real tutor feedback, and a certificate. Join the next cohort.
Join CareerEx