Mastering MongoDB with Mongoose: Advanced Relationships and Performance Optimization
Explore advanced MongoDB and Mongoose concepts in the MERN stack, including relationships, query optimization, and performance techniques for scalable applications.
StalkTechie
Author
Mastering MongoDB with Mongoose: Advanced Relationships and Performance Optimization
Mongoose provides an elegant way to model MongoDB data and is a core part of the MERN stack. This guide dives into advanced relationships, query optimization, and best practices to supercharge your MongoDB and Mongoose usage.
Understanding MongoDB Relationships
MongoDB's document-based nature means that relationships are often modeled differently from traditional relational databases. We'll explore how to set up and query advanced relationships using Mongoose's features.
Basic Relationship Types
// One-to-One Relationship (Embedded Document)
const userSchema = new mongoose.Schema({
name: String,
profile: {
type: mongoose.Schema.Types.ObjectId,
ref: "Profile"
}
});
const User = mongoose.model("User", userSchema);
// One-to-Many Relationship (Array of References)
const postSchema = new mongoose.Schema({
title: String,
content: String,
comments: [{ type: mongoose.Schema.Types.ObjectId, ref: "Comment" }]
});
const Post = mongoose.model("Post", postSchema);
// Many-to-Many Relationship (Referenced Documents)
const userSchema2 = new mongoose.Schema({
name: String,
roles: [{ type: mongoose.Schema.Types.ObjectId, ref: "Role" }]
});
const User2 = mongoose.model("User", userSchema2);
Advanced Relationship Patterns
Populating Related Data
One of the most powerful Mongoose features is its ability to populate related documents.
// Populating one-to-many relationships
const post = await Post.findOne({ _id: postId }).populate("comments");
// Populating many-to-many relationships
const user = await User.findOne({ _id: userId }).populate("roles");
// Populating nested relationships
const postNested = await Post.findOne({ _id: postId })
.populate({
path: "comments",
populate: { path: "user", select: "name avatar" }
});
Virtuals for Non-Stored Relationships
In MongoDB, we often use virtual fields to represent non-stored relationships.
// Virtual for calculating the number of comments on a post
postSchema.virtual("commentCount").get(function () {
return this.comments.length;
});
postSchema.set("toObject", { virtuals: true });
postSchema.set("toJSON", { virtuals: true });
Performance Optimization Techniques
Query Optimization and Indexing
// Create an index on the "status" field for faster queries
PostSchema.index({ status: 1 });
// Use compound indexes for queries involving multiple fields
PostSchema.index({ userId: 1, createdAt: -1 });
Lean Queries for Faster Data Retrieval
// Use lean for faster queries
const posts = await Post.find().lean().exec();
// Lean with populating related documents
const postsPopulated = await Post.find()
.populate("user")
.lean()
.exec();
Avoiding N+1 Query Problem
// Use aggregation to reduce N+1 queries
const posts = await Post.aggregate([
{ $match: { status: "published" } },
{ $lookup: { from: "users", localField: "userId", foreignField: "_id", as: "user" } },
{ $unwind: "$user" }
]);
Optimizing Large Data Sets with Pagination
// Simple pagination example
const posts = await Post.find()
.skip((page - 1) * pageSize)
.limit(pageSize)
.exec();
// Pagination with sorting
const postsSorted = await Post.find()
.sort({ createdAt: -1 })
.skip((page - 1) * pageSize)
.limit(pageSize)
.exec();
Advanced Mongoose Features
Middleware for Pre/Post Operations
// Pre-save hook for password hashing
userSchema.pre("save", function (next) {
if (this.isModified("password")) {
this.password = bcrypt.hashSync(this.password, 10);
}
next();
});
// Post-save hook for logging
userSchema.post("save", function (doc) {
console.log(`User ${doc.name} saved to database`);
});
Aggregation Framework for Complex Queries
// Aggregate posts by category
const postsByCategory = await Post.aggregate([
{ $group: { _id: "$category", totalPosts: { $sum: 1 } } }
]);
// Calculate total likes for each post
const postLikes = await Post.aggregate([
{ $match: { status: "published" } },
{ $lookup: { from: "likes", localField: "_id", foreignField: "postId", as: "likes" } },
{ $project: { title: 1, likesCount: { $size: "$likes" } } }
]);
Real-World Example: Blog Application
const express = require("express");
const router = express.Router();
const Post = require("../models/Post");
// Get all posts with populated user data and comments
router.get("/posts", async (req, res) => {
try {
const posts = await Post.find()
.populate("user", "name avatar")
.populate({
path: "comments",
populate: { path: "user", select: "name avatar" }
})
.lean()
.exec();
res.json(posts);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Get a single post with comments and user
router.get("/posts/:id", async (req, res) => {
try {
const post = await Post.findById(req.params.id)
.populate("user", "name avatar")
.populate("comments")
.lean()
.exec();
res.json(post);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
Best Practice Summary
- Always use .lean() for read-only queries
- Index fields that are frequently queried
- Use aggregation for complex queries
- Paginate large datasets
- Utilize Mongoose middleware for hooks