Mongoose In-Depth
Mongoose is a MongoDB Object Data Modeling (ODM) library for Node.js. It provides a powerful
framework to model your data, define schemas, perform CRUD operations, manage relationships
between different models, and add validation, middleware, and more. In this in-depth explanation, we
will dive into key concepts, features, and advanced topics in Mongoose.
1. Mongoose Basics
1.1 Installation and Setup
Mongoose is an npm package that can be installed in your Node.js project.
npm install mongoose
You need to establish a connection to MongoDB using mongoose.connect():
const mongoose = require('mongoose');
// Connect to MongoDB
mongoose.connect('mongodb://localhost/mydatabase', { useNewUrlParser: true, useUnifiedTopology:
true })
.then(() => console.log('MongoDB connected'))
.catch(err => console.log('MongoDB connection error: ', err));
2. Schema and Model in Mongoose
2.1 Schema Definition
A schema defines the structure of the documents within a MongoDB collection. It outlines the fields and
their types, validation, default values, and more.
const mongoose = require('mongoose');
// Define a schema
const userSchema = new mongoose.Schema({
name: {
type: String,
required: true,
},
email: {
type: String,
required: true,
unique: true,
},
age: {
type: Number,
min: 18, // Validation: age should be 18 or older
},
createdAt: {
type: Date,
default: Date.now,
},
});
// Create a model based on the schema
const User = mongoose.model('User', userSchema);
module.exports = User;
2.2 Model Creation
A model is a constructor function that allows us to create and interact with documents that follow the
schema.
const User = mongoose.model('User', userSchema);
After defining the model, you can perform operations like creating, reading, updating, and deleting
(CRUD) documents in the associated MongoDB collection.
3. CRUD Operations with Mongoose
3.1 Create Operation
To create a new document, you can use the save() method:
const newUser = new User({
name: 'Alice',
email: 'alice@example.com',
age: 30,
});
newUser.save()
.then(user => console.log('User created:', user))
.catch(err => console.log('Error creating user:', err));
You can also use create() to simplify the creation process:
User.create({
name: 'Bob',
email: 'bob@example.com',
age: 25,
})
.then(user => console.log('User created:', user))
.catch(err => console.log('Error creating user:', err));
3.2 Read Operation
To find documents, use the find(), findOne(), or findById() methods.
// Find all users
User.find()
.then(users => console.log('All Users:', users))
.catch(err => console.log('Error finding users:', err));
// Find a user by email
User.findOne({ email: 'alice@example.com' })
.then(user => console.log('Found User:', user))
.catch(err => console.log('Error finding user:', err));
// Find a user by ID
User.findById('60c7f8f5b5f0d44b88abec30')
.then(user => console.log('User by ID:', user))
.catch(err => console.log('Error finding user:', err));
3.3 Update Operation
You can update documents using methods like update(), updateOne(), findByIdAndUpdate(), etc.
// Update a user by email
User.updateOne({ email: 'alice@example.com' }, { age: 31 })
.then(result => console.log('Update result:', result))
.catch(err => console.log('Error updating user:', err));
// Update a user and return the updated document
User.findByIdAndUpdate('60c7f8f5b5f0d44b88abec30', { age: 32 }, { new: true })
.then(user => console.log('Updated User:', user))
.catch(err => console.log('Error updating user:', err));
3.4 Delete Operation
To delete a document, you can use deleteOne(), deleteMany(), or findByIdAndDelete():
// Delete a user by ID
User.findByIdAndDelete('60c7f8f5b5f0d44b88abec30')
.then(result => console.log('Deleted User:', result))
.catch(err => console.log('Error deleting user:', err));
4. Mongoose Query Helpers
Mongoose provides built-in methods to handle complex queries such as pagination, sorting, and
filtering.
4.1 Sorting and Pagination
User.find()
.sort({ createdAt: -1 }) // Sort by createdAt in descending order
.limit(10) // Limit results to 10 users
.skip(10) // Skip first 10 users for pagination
.exec((err, users) => {
if (err) {
console.log(err);
} else {
console.log('Paginated Users:', users);
});
4.2 Filtering
You can use query operators such as $gt, $lt, $in, $ne, etc., to filter results.
User.find({ age: { $gte: 18 } })
.then(users => console.log('Adult Users:', users))
.catch(err => console.log('Error finding users:', err));
5. Mongoose Middleware
Middleware in Mongoose can run before or after certain actions, such as saving, updating, or removing
documents. This can be useful for data validation, logging, or modifying data before it is saved.
5.1 Pre and Post Middleware
• Pre Middleware: Runs before the action is performed (e.g., before saving a document).
• Post Middleware: Runs after the action is performed (e.g., after a document is saved).
// Pre-save middleware for hashing passwords
userSchema.pre('save', async function (next) {
if (this.isModified('password')) {
this.password = await bcrypt.hash(this.password, 8);
next();
});
// Post-save middleware
userSchema.post('save', function (doc, next) {
console.log('New user saved:', doc);
next();
});
5.2 Validation Middleware
You can use middleware to validate the data before saving.
userSchema.pre('save', function (next) {
if (!this.email.includes('@')) {
return next(new Error('Email is not valid'));
next();
});
6. Mongoose Virtuals
A virtual is a field that is not stored in the database but is derived from other fields. It's often used for
computed properties.
6.1 Defining a Virtual
userSchema.virtual('fullName').get(function () {
return this.firstName + ' ' + this.lastName;
});
This will add a fullName property to the document without saving it to the database.
7. Mongoose Population
Population is the process of replacing fields that store ObjectIds with the actual documents that are
referenced.
7.1 Example of Population
Imagine you have a Post model that has a reference to the User model for the author.
const postSchema = new mongoose.Schema({
title: String,
content: String,
author: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User', // Reference to the User model
},
});
const Post = mongoose.model('Post', postSchema);
// Populate the author field with user data
Post.find()
.populate('author') // Populate the author field with the actual user data
.exec((err, posts) => {
if (err) console.log(err);
else console.log(posts);
});
8. Mongoose Indexing
Indexing can improve the performance of queries that are frequently used, especially for large datasets.
8.1 Defining an Index
userSchema.index({ email: 1 }); // Index on the 'email' field
You can also define compound indexes and set options like uniqueness.
9. Advanced Mongoose Features
9.1 Aggregation Framework
Mongoose supports the MongoDB aggregation framework, which allows you to perform complex
queries, including grouping, sorting, and transforming data.
User.aggregate([
{ $match: { age: { $gte: 18 } } },
{ $group: { _id: null, averageAge: { $avg: '$age' } } }
])
.then(result => console.log('Average Age:', result))
.catch(err => console.log('Error in aggregation:', err));
9.2 Transactions
Mongoose supports transactions in MongoDB to handle multi-step operations that need to be
committed together or rolled back.
const session = await mongoose.startSession();
session.startTransaction();
try {
await User.create([{ name: 'Alice', email: 'alice@example.com' }], { session });
await session.commitTransaction();
} catch (error) {
await session.abortTransaction();
console.error('Transaction aborted:', error);
} finally {
session.endSession();
10. Conclusion
Mongoose is a powerful tool that simplifies interacting with MongoDB from a Node.js environment. It
provides a rich set of features such as schema-based modeling, middleware, population, validation, and
aggregation, making it easier to manage and manipulate data. Understanding how to use Mongoose
effectively will help you build robust applications with MongoDB.