MongoDB Relationships
Modelling Relationships in MongoDB
MongoDB supports two primary strategies for representing relationships: embedding (denormalization) and referencing (normalization). Unlike SQL, there are no foreign key constraints — you manage relationships in application logic or through the aggregation pipeline.
Embedded Documents
// ONE-TO-ONE EMBEDDED: User with address
{
"_id": ObjectId("u1"),
"name": "Alice Johnson",
"address": { "street": "123 Main St", "city": "New York", "zip": "10001" }
}
// ONE-TO-MANY EMBEDDED: Post with comments (bounded array)
{
"_id": ObjectId("p1"),
"title": "Getting Started with MongoDB",
"comments": [
{ "user": "Bob", "text": "Great article!", "date": ISODate("2024-01-10") },
{ "user": "Carol", "text": "Very helpful.", "date": ISODate("2024-01-11") }
]
}
// Query embedded field
db.posts.find({ "comments.user": "Bob" })
// Update embedded array element using positional operator
db.posts.updateOne(
{ _id: ObjectId("p1"), "comments.user": "Bob" },
{ $set: { "comments.$.text": "Updated comment!" } }
)
Manual References
// users collection
{ "_id": ObjectId("u1"), "name": "Alice Johnson", "email": "alice@example.com" }
// orders collection — each order references the user
{ "_id": ObjectId("o1"), "userId": ObjectId("u1"), "total": 1299.99, "status": "shipped" }
{ "_id": ObjectId("o2"), "userId": ObjectId("u1"), "total": 45.00, "status": "pending" }
// Join using $lookup aggregation
db.users.aggregate([
{ $match: { _id: ObjectId("u1") } },
{ $lookup: {
from: "orders",
localField: "_id",
foreignField: "userId",
as: "orders"
}},
{ $project: { name: 1, email: 1, orderCount: { $size: "$orders" }, orders: 1 } }
])
Many-to-Many Relationships
// Students and Courses — many-to-many
// students collection
{
"_id": ObjectId("s1"),
"name": "Bob Smith",
"enrolledCourses": [ObjectId("c1"), ObjectId("c2")]
}
// courses collection
{
"_id": ObjectId("c1"),
"title": "MongoDB Fundamentals",
"enrolledStudents": [ObjectId("s1"), ObjectId("s2")]
}
// Find all courses a student is enrolled in
db.courses.find({ _id: { $in: [ObjectId("c1"), ObjectId("c2")] } })
// Add a student to a course
db.courses.updateOne(
{ _id: ObjectId("c1") },
{ $addToSet: { enrolledStudents: ObjectId("s3") } }
)
db.students.updateOne(
{ _id: ObjectId("s3") },
{ $addToSet: { enrolledCourses: ObjectId("c1") } }
)
// EMBED when:
// - Data is always accessed together with the parent
// - The sub-document is small and bounded (e.g., max 10-20 items)
// - The sub-document is not shared across multiple parents
// - You want atomic reads/writes in a single operation
// REFERENCE when:
// - Data is accessed independently of the parent
// - The array could grow unboundedly (e.g., all comments on a viral post)
// - The same data is referenced by multiple documents
// - The sub-document is large and rarely needed
// Example: User profile — EMBED (always needed together)
{ "_id": ObjectId("u1"), "name": "Alice", "profile": { "bio": "...", "avatar": "..." } }
// Example: User orders — REFERENCE (many orders, accessed separately)
// orders: { userId: ObjectId("u1"), total: 99.99, ... }
// db.orders.find({ userId: ObjectId("u1") })