Mongoose 中间件:前置和后置钩子
您将编写文档和查询中间件钩子,用于在保存前对密码进行哈希处理或在查找后记录日志等任务。
Mongoose 中间件:前置和后置钩子 是 CoddyKit 上的免费 MongoDB Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 MongoDB Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 MongoDB Academy 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
What Is Mongoose Middleware?
Mongoose middleware (also called hooks) are functions that run before or after specific operations like save, find, updateOne, deleteOne, and more. They enable you to inject custom logic into the lifecycle of document and query operations without cluttering your route handlers. Common uses include hashing passwords before save, logging query times, enforcing soft deletes, and populating related data after find.
Two Types: Document and Query Middleware
Mongoose has two distinct categories of middleware: Document middleware hooks into operations on a specific Document instance (save, validate, remove, init). Query middleware hooks into query operations invoked on the Model (find, findOne, updateOne, deleteOne, etc.). The key difference is what this refers to—in document middleware, this is the document; in query middleware, this is the query object.
// Document middleware: 'this' = the document
userSchema.pre('save', function () {
console.log('Saving document:', this.email);
});
// Query middleware: 'this' = the Query object
userSchema.pre('find', function () {
console.log('Running query:', this.getQuery());
});Pre-save: Hashing Passwords
The pre-save hook is the most common document middleware. It runs before a document is saved to MongoDB. The canonical use case is hashing passwords: when a user document is saved with a new or modified password, hash it with bcrypt before storing. The this.isModified('password') check prevents re-hashing an already-hashed password on unrelated saves.
const bcrypt = require('bcrypt');
userSchema.pre('save', async function () {
// 'this' is the User document being saved
if (!this.isModified('password')) {
return; // skip if password hasn't changed
}
const saltRounds = 12;
this.password = await bcrypt.hash(this.password, saltRounds);
// The hashed value replaces the plain password before MongoDB stores it
});isNew and isModified Helpers
Document middleware has access to tracking helpers: this.isNew is true when the document is being inserted for the first time (not updated). this.isModified(path) returns true if the specified field has been changed since the document was last saved or fetched. These helpers let you run hooks conditionally—only on creation, or only when a specific field changes.
userSchema.pre('save', async function () {
if (this.isNew) {
// Only runs when creating a new user, not on updates
this.verificationToken = crypto.randomBytes(32).toString('hex');
this.verificationExpires = new Date(Date.now() + 24 * 60 * 60 * 1000);
}
if (this.isModified('email')) {
// Only runs when the email field specifically changed
this.emailVerified = false; // reset verification on email change
}
});Post-save: Side Effects After Saving
Post-save hooks run after a document is successfully persisted. They receive the saved document as the first argument and (in older Mongoose) a next callback. Post hooks are ideal for side effects that should happen after a successful save: sending a welcome email, updating a search index, publishing an event to a message queue, or clearing a cache. Errors in post hooks do not roll back the save.
userSchema.post('save', async function (doc) {
// 'doc' is the saved document
if (doc.isNew) {
// Note: 'isNew' is false here (doc was just saved, so it's no longer new)
// Track this with a flag set in pre-save:
}
});
// Pattern: set a flag in pre-save, read it in post-save
userSchema.pre('save', function () {
this._wasNew = this.isNew; // save state before it changes
});
userSchema.post('save', async function (doc) {
if (doc._wasNew) {
await sendWelcomeEmail(doc.email, doc.name);
await analyticsTracker.track('user_created', { userId: doc._id });
}
});Query Middleware: Pre-find for Soft Deletes
A classic query middleware pattern is implementing soft deletes. Instead of removing documents, set a deletedAt field. Then add a pre('find') hook that automatically adds { deletedAt: null } to every find query, ensuring deleted documents are never returned by default. This provides an audit trail while making the soft-delete logic transparent to the rest of the application.
const postSchema = new mongoose.Schema({
title: String,
content: String,
deletedAt: { type: Date, default: null }
});
// Automatically exclude soft-deleted documents from all find queries
postSchema.pre(/^find/, function () {
// 'this' is the Query object
this.where({ deletedAt: null });
// /^find/ matches find, findOne, findOneAndUpdate, etc.
});
// Now Post.find({}) never returns deleted posts
// To explicitly query deleted posts, you'd call Post.find({}).bypassMiddleware() or use .lean() with the native driverQuery Middleware: Pre-find Automatic Population
You can use query middleware to automatically populate a reference field on every find. This ensures that a referenced document is always resolved without requiring callers to add .populate() to every query. While convenient, be careful—automatic population adds a second query for every find and can degrade performance if the reference is large or not always needed.
const reviewSchema = new mongoose.Schema({
productId: { type: mongoose.Schema.Types.ObjectId, ref: 'Product' },
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
rating: Number,
comment: String
});
// Always populate author info on find
reviewSchema.pre(/^find/, function () {
this.populate({
path: 'userId',
select: 'name avatar'
});
});
// Now Review.find() always includes user name and avatarPre-deleteOne: Cascade Deletes
Mongoose does not enforce cascade deletes (deleting related documents when a parent is deleted) automatically. You can implement cascade behavior using document middleware. A pre-deleteOne hook on a User model can delete all posts, comments, and sessions belonging to that user before the user document itself is removed. This keeps referential integrity without foreign key constraints.
userSchema.pre('deleteOne', { document: true, query: false }, async function () {
// 'this' is the User document being deleted
const userId = this._id;
// Cascade delete related documents
await Promise.all([
Post.deleteMany({ authorId: userId }),
Comment.deleteMany({ userId: userId }),
Session.deleteMany({ userId: userId }),
Notification.deleteMany({ userId: userId })
]);
console.log('Cascade deleted data for user:', userId);
});
// Trigger:
// const user = await User.findById(id);
// await user.deleteOne(); // triggers pre-deleteOne aboveMiddleware Error Handling
If a pre-hook function throws an error or rejects a Promise, the operation it precedes is aborted. This lets you perform validation or authorization checks in middleware and abort saves or queries by throwing. For example, a pre-save hook that validates business logic (not just schema validation) can throw an error that bubbles up to the .save() call's catch block in the application code.
orderSchema.pre('save', async function () {
if (this.total <= 0) {
throw new Error('Order total must be positive');
}
// Check inventory synchronously before saving the order
const product = await Product.findById(this.productId).lean();
if (!product || product.stock < this.quantity) {
throw new Error('Insufficient inventory for this order');
}
});
// In route handler:
try {
const order = new Order({ productId, quantity, total });
await order.save(); // throws if pre-save hook rejects
} catch (err) {
res.status(400).json({ error: err.message });
}Aggregate Middleware
Mongoose also supports middleware for aggregation pipelines. A pre-aggregate hook gives you access to the pipeline array before it is sent to MongoDB, allowing you to prepend stages (like filtering soft-deleted documents) or append stages (like injecting a default limit). Access the pipeline via this.pipeline() inside the hook function.
postSchema.pre('aggregate', function () {
// 'this' is the Aggregate object
// Add a $match stage at the beginning to exclude soft-deleted documents
this.pipeline().unshift({
$match: { deletedAt: null }
});
});
// Now Post.aggregate([...]) automatically excludes deleted posts
// at the start of every aggregation pipelineMiddleware Pitfalls: Query Methods That Bypass Hooks
Not all write operations trigger document middleware. updateMany(), findOneAndUpdate(), replaceOne() called on the Model (not an instance) bypass document save hooks—they are query middleware and need separate hooks if you want to intercept them. For example, a pre-save password hashing hook does NOT run when you call User.updateOne({}, { $set: { password: plain } }). Always hash in the application code for query-based updates.
// WRONG: password NOT hashed — bypasses pre-save hook
await User.updateOne({ _id: userId }, { $set: { password: plainPassword } });
// RIGHT for query-level updates: hash before calling updateOne
const hashed = await bcrypt.hash(plainPassword, 12);
await User.updateOne({ _id: userId }, { $set: { password: hashed } });
// Or: fetch, modify, save — triggers pre-save hook
const user = await User.findById(userId);
user.password = plainPassword; // hook will hash it
await user.save();Quick Check
Test your understanding of MongoDB & NoSQL Databases concepts from this lesson.
Lesson Recap
In this lesson you learned: pre hooks run before an operation and can abort it by throwing an error; post hooks run after and receive the result as an argument, document middleware (pre-save, pre-deleteOne) uses 'this' as the document, while query middleware uses 'this' as the Query object, and query-level write methods (updateOne, updateMany, findOneAndUpdate) bypass document middleware — always be aware of which hooks fire for each operation type. This completes the MongoDB & NoSQL Databases course track!
用 AI 导师学习 JavaScript — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 30
- 课程
- 120
常见问题解答
「Mongoose 中间件:前置和后置钩子」课时是免费的吗?
是的 — 「Mongoose 中间件:前置和后置钩子」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 MongoDB Academy 课程的其余内容,请升级到 CoddyKit PRO。 MongoDB Academy 课程共包含 4 节课。
「Mongoose 中间件:前置和后置钩子」这节课中我会学到什么?
您将编写文档和查询中间件钩子,用于在保存前对密码进行哈希处理或在查找后记录日志等任务。 你通过在浏览器中直接运行的动手代码来练习 MongoDB Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 MongoDB Academy 需要有经验吗?
无需任何先前经验。CoddyKit 上的 MongoDB Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。
「Mongoose 中间件:前置和后置钩子」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 MongoDB Academy 课中编写并运行代码吗?
能。每节 MongoDB Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用官方 Node.js 驱动程序连接
- Mongoose 模式、模型和虚拟属性
- Mongoose 查询、链式调用和精简文档
- Mongoose 中间件:前置和后置钩子