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.

S

StalkTechie

Author

March 29, 2025
1530 views

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
Share this post:

Related Articles

Discussion

0 comments

Please log in to join the discussion.

Login to Comment