Skip to content

MongoDB - Relationships

In MongoDB, relationships signify how different documents are logically connected. These connections can be modeled using two primary strategies: Embedding (denormalization) and Referencing (normalization). The choice of strategy depends on your application’s data access patterns, and it can model one-to-one (1:1), one-to-many (1:N), and many-to-many (N:N) relationships.

Let’s explore these concepts using a common scenario: a user who has multiple addresses. This represents a one-to-many (1:N) relationship.

A modern user document might look like this:

// users collection
{
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate()
}

And an associated address document:

// addresses collection (for referenced model)
{
"_id": new ObjectId(),
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
}

Strategy 1: Modeling with Embedded Documents

Section titled “Strategy 1: Modeling with Embedded Documents”

In the embedded approach, you place the related data directly inside the parent document. For our user and addresses, we would embed an array of address sub-documents within the user document.

// A user with embedded addresses
db.users.insertOne({
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate(),
"addresses": [
{
"type": "Home",
"street": "123 Main St",
"city": "Anytown",
"state": "CA",
"zip": "12345"
},
{
"type": "Work",
"street": "456 Business Ave",
"city": "Metropolis",
"state": "NY",
"zip": "54321"
}
]
});

This approach consolidates all related data into a single document, offering significant performance benefits for read operations. The entire user profile, including all addresses, can be fetched with a single query:

// Retrieve a user and their addresses in one go
db.users.findOne({ "email": "alex.doe@example.com" });

Pros: Faster reads (no database joins needed), data locality, and atomic updates for the entire document. Cons: Can lead to large documents (max 16MB BSON document size), data duplication if the embedded information is also needed elsewhere, and potentially slower writes if the embedded arrays grow very large.

Strategy 2: Modeling with Document References

Section titled “Strategy 2: Modeling with Document References”

The referencing (or normalized) approach keeps related data in separate collections. The parent document stores references—typically the _id values—to the related documents.

// User document referencing address IDs
db.users.insertOne({
"_id": new ObjectId(),
"name": "Alex Doe",
"email": "alex.doe@example.com",
"createdAt": new ISODate(),
"address_ids": [
new ObjectId("615cba23...1"), // ID of the first address document
new ObjectId("615cba23...2") // ID of the second address document
]
});
// Address documents in a separate collection
db.addresses.insertMany([
{ "_id": new ObjectId("615cba23...1"), "street": "123 Main St", ... },
{ "_id": new ObjectId("615cba23...2"), "street": "456 Business Ave", ... }
]);

To retrieve the user’s addresses, you would use the $lookup aggregation stage, which performs a left outer join to the other collection. This is the modern, efficient way to handle references.

// Using $lookup to join users and addresses
db.users.aggregate([
{
$match: { "email": "alex.doe@example.com" } // Find the user
},
{
$lookup: {
from: "addresses", // The collection to join with
localField: "address_ids", // Field from the input documents (users)
foreignField: "_id", // Field from the documents of the 'from' collection
as: "address_info" // The new array field to add to the input documents
}
}
]).pretty();

Pros: Avoids data duplication, keeps documents smaller, and is better when related data is frequently updated or accessed on its own. It’s more flexible for complex N:N relationships. Cons: Requires an additional $lookup operation, which can be slower than reading a single embedded document. Retrieving a complete entity requires multiple collection accesses.

The choice is not always binary. A hybrid approach is often best. Use embedding for relationships that are contained, have a clear parent-child dynamic, and are primarily accessed together (e.g., comments on a blog post). Use referencing when the related data is large, accessed independently, or part of a many-to-many relationship (e.g., products and categories).