中间件
中间件(也称为预处理和后处理 _钩子_)是在执行异步函数期间传递控制的函数。中间件在模式级别指定,对于编写 插件 很有用。
- 中间件类型
- 预处理
- 预处理钩子中的错误
- 后处理
- 异步后处理钩子
- 在编译模型之前定义中间件
- 保存/验证钩子
- 在中间件中访问参数
- 命名冲突
- 关于 findAndUpdate() 和查询中间件的说明
- 错误处理中间件
- 聚合钩子
- 同步钩子
中间件类型
Mongoose 有 4 种类型的中间件:文档中间件、模型中间件、聚合中间件和查询中间件。
文档中间件支持以下文档函数。在 Mongoose 中,文档是 Model
类的实例。在文档中间件函数中,this
指的是文档。要访问模型,请使用 this.constructor
。
查询中间件支持以下查询函数。当您在查询对象上调用 exec()
或 then()
,或在查询对象上调用 await
时,查询中间件会执行。在查询中间件函数中,this
指的是查询。
- count
- countDocuments
- deleteMany
- deleteOne
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
- replaceOne
- updateOne
- updateMany
- validate
聚合中间件用于 MyModel.aggregate()
。当您在聚合对象上调用 exec()
时,聚合中间件会执行。在聚合中间件中,this
指的是 聚合对象。
模型中间件支持以下模型函数。不要将模型中间件与文档中间件混淆:模型中间件挂钩到 Model
类上的 _静态_ 函数,文档中间件挂钩到 Model
类上的 _方法_。在模型中间件函数中,this
指的是模型。
以下是可以传递给 pre()
的可能字符串
- aggregate
- bulkWrite
- count
- countDocuments
- createCollection
- deleteOne
- deleteMany
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
- init
- insertMany
- replaceOne
- save
- update
- updateOne
- updateMany
- validate
所有中间件类型都支持预处理和后处理钩子。预处理和后处理钩子的工作原理将在下面详细介绍。
注意: Mongoose 默认在 Query.prototype.updateOne()
上注册 updateOne
中间件。这意味着 doc.updateOne()
和 Model.updateOne()
都将触发 updateOne
钩子,但 this
指的是查询,而不是文档。要将 updateOne
中间件注册为文档中间件,请使用 schema.pre('updateOne', { document: true, query: false })
。
注意: 与 updateOne
一样,Mongoose 默认在 Query.prototype.deleteOne
上注册 deleteOne
中间件。这意味着 Model.deleteOne()
将触发 deleteOne
钩子,并且 this
将指向查询。但是,由于历史原因,doc.deleteOne()
不会触发 deleteOne
查询中间件。要将 deleteOne
中间件注册为文档中间件,请使用 schema.pre('deleteOne', { document: true, query: false })
。
注意: create()
函数会触发 save()
钩子。
注意: 查询中间件不会在子文档上执行。
const childSchema = new mongoose.Schema({
name: String
});
const mainSchema = new mongoose.Schema({
child: [childSchema]
});
mainSchema.pre('findOneAndUpdate', function() {
console.log('Middleware on parent document'); // Will be executed
});
childSchema.pre('findOneAndUpdate', function() {
console.log('Middleware on subdocument'); // Will not be executed
});
预处理
预处理中间件函数按顺序执行,当每个中间件调用 next
时。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
在 mongoose 5.x 中,您无需手动调用 next()
,而是可以使用返回 Promise 的函数。特别是,您可以使用 async/await
。
schema.pre('save', function() {
return doStuff().
then(() => doMoreStuff());
});
// Or, using async functions
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
如果您使用 next()
,next()
调用 不会阻止您中间件函数中的其余代码执行。使用 早期 return
模式 来防止在调用 next()
时,您的中间件函数的其余部分运行。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
if (foo()) {
console.log('calling next!');
// `return next();` will make sure the rest of this function doesn't run
/* return */ next();
}
// Unless you comment out the `return` above, 'after next' will print
console.log('after next');
});
用例
中间件对于原子化模型逻辑很有用。以下是其他一些想法
- 复杂验证
- 删除依赖文档(删除用户会删除其所有博客文章)
- 异步默认值
- 特定操作触发的异步任务
预处理钩子中的错误
如果任何预处理钩子出错,mongoose 不会执行后续中间件或钩子函数。mongoose 将改为将错误传递给回调并/或拒绝返回的 Promise。在中间件中报告错误有几种方法
schema.pre('save', function(next) {
const err = new Error('something went wrong');
// If you call `next()` with an argument, that argument is assumed to be
// an error.
next(err);
});
schema.pre('save', function() {
// You can also return a promise that rejects
return new Promise((resolve, reject) => {
reject(new Error('something went wrong'));
});
});
schema.pre('save', function() {
// You can also throw a synchronous error
throw new Error('something went wrong');
});
schema.pre('save', async function() {
await Promise.resolve();
// You can also throw an error in an `async` function
throw new Error('something went wrong');
});
// later...
// Changes will not be persisted to MongoDB because a pre hook errored out
myDoc.save(function(err) {
console.log(err.message); // something went wrong
});
多次调用 next()
是一个空操作。如果您使用错误 err1
调用 next()
,然后抛出错误 err2
,mongoose 将报告 err1
。
后处理中间件
post 中间件在钩子方法及其所有 pre
中间件完成后执行。
schema.post('init', function(doc) {
console.log('%s has been initialized from the db', doc._id);
});
schema.post('validate', function(doc) {
console.log('%s has been validated (but not saved yet)', doc._id);
});
schema.post('save', function(doc) {
console.log('%s has been saved', doc._id);
});
schema.post('deleteOne', function(doc) {
console.log('%s has been deleted', doc._id);
});
异步后处理钩子
如果您的后处理钩子函数至少接受 2 个参数,mongoose 将假设第二个参数是您将调用的 next()
函数,以触发序列中的下一个中间件。
// Takes 2 parameters: this is an asynchronous post hook
schema.post('save', function(doc, next) {
setTimeout(function() {
console.log('post1');
// Kick off the second post hook
next();
}, 10);
});
// Will not execute until the first middleware calls `next()`
schema.post('save', function(doc, next) {
console.log('post2');
next();
});
您还可以将异步函数传递给 post()
。如果您传递一个至少接受 2 个参数的异步函数,您仍然需要负责调用 next()
。但是,您也可以传递一个接受不到 2 个参数的异步函数,Mongoose 将等待 Promise 解析。
schema.post('save', async function(doc) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('post1');
// If less than 2 parameters, no need to call `next()`
});
schema.post('save', async function(doc, next) {
await new Promise(resolve => setTimeout(resolve, 1000));
console.log('post1');
// If there's a `next` parameter, you need to call `next()`.
next();
});
在编译模型之前定义中间件
在 编译模型 后调用 pre()
或 post()
在 Mongoose 中通常不起作用。例如,以下 pre('save')
中间件不会触发。
const schema = new mongoose.Schema({ name: String });
// Compile a model from the schema
const User = mongoose.model('User', schema);
// Mongoose will **not** call the middleware function, because
// this middleware was defined after the model was compiled
schema.pre('save', () => console.log('Hello from pre save'));
const user = new User({ name: 'test' });
user.save();
这意味着您必须在调用 mongoose.model()
之前添加所有中间件和 插件。以下脚本将打印“来自 pre save 的问候”。
const schema = new mongoose.Schema({ name: String });
// Mongoose will call this middleware function, because this script adds
// the middleware to the schema before compiling the model.
schema.pre('save', () => console.log('Hello from pre save'));
// Compile a model from the schema
const User = mongoose.model('User', schema);
const user = new User({ name: 'test' });
user.save();
因此,小心从您定义模式的相同文件中导出 Mongoose 模型。如果您选择使用此模式,则必须在调用 require()
访问您的模型文件之前定义 全局插件。
const schema = new mongoose.Schema({ name: String });
// Once you `require()` this file, you can no longer add any middleware
// to this schema.
module.exports = mongoose.model('User', schema);
保存/验证钩子
save()
函数会触发 validate()
钩子,因为 mongoose 具有一个内置的 pre('save')
钩子,该钩子会调用 validate()
。这意味着所有 pre('validate')
和 post('validate')
钩子都将在任何 pre('save')
钩子之前被调用。
schema.pre('validate', function() {
console.log('this gets printed first');
});
schema.post('validate', function() {
console.log('this gets printed second');
});
schema.pre('save', function() {
console.log('this gets printed third');
});
schema.post('save', function() {
console.log('this gets printed fourth');
});
在中间件中访问参数
Mongoose 提供了两种方法来获取有关触发中间件的函数调用的信息。对于查询中间件,我们建议使用 this
,它将是一个 Mongoose 查询实例。
const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('findOneAndUpdate', function() {
console.log(this.getFilter()); // { name: 'John' }
console.log(this.getUpdate()); // { age: 30 }
});
const User = mongoose.model('User', userSchema);
await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } });
对于文档中间件(如 pre('save')
),Mongoose 将作为 pre('save')
回调的第二个参数,传递给 save()
的第一个参数。您应该使用第二个参数来访问 save()
调用的 options
,因为 Mongoose 文档不会存储您可以传递给 save()
的所有选项。
const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('save', function(next, options) {
options.validateModifiedOnly; // true
// Remember to call `next()` unless you're using an async function or returning a promise
next();
});
const User = mongoose.model('User', userSchema);
const doc = new User({ name: 'John', age: 30 });
await doc.save({ validateModifiedOnly: true });
命名冲突
Mongoose 同时为 deleteOne()
提供查询钩子和文档钩子。
schema.pre('deleteOne', function() { console.log('Removing!'); });
// Does **not** print "Removing!". Document middleware for `deleteOne` is not executed by default
await doc.deleteOne();
// Prints "Removing!"
await Model.deleteOne();
您可以将选项传递给 Schema.pre()
和 Schema.post()
以切换 Mongoose 是否会为 Document.prototype.deleteOne()
或 Query.prototype.deleteOne()
调用您的 deleteOne()
钩子。请注意,您需要在传递的对象中设置 document
和 query
属性。
// Only document middleware
schema.pre('deleteOne', { document: true, query: false }, function() {
console.log('Deleting doc!');
});
// Only query middleware. This will get called when you do `Model.deleteOne()`
// but not `doc.deleteOne()`.
schema.pre('deleteOne', { query: true, document: false }, function() {
console.log('Deleting!');
});
Mongoose 也同时为 validate()
提供查询钩子和文档钩子。与 deleteOne
和 updateOne
不同,validate
中间件默认应用于 Document.prototype.validate
。
const schema = new mongoose.Schema({ name: String });
schema.pre('validate', function() {
console.log('Document validate');
});
schema.pre('validate', { query: true, document: false }, function() {
console.log('Query validate');
});
const Test = mongoose.model('Test', schema);
const doc = new Test({ name: 'foo' });
// Prints "Document validate"
await doc.validate();
// Prints "Query validate"
await Test.find().validate();
关于 findAndUpdate()
和查询中间件的说明
预处理和后处理 save()
钩子不会在 update()
、findOneAndUpdate()
等上执行。您可以在 此 GitHub 问题 中看到关于原因的更详细的讨论。Mongoose 4.0 为这些函数引入了不同的钩子。
schema.pre('find', function() {
console.log(this instanceof mongoose.Query); // true
this.start = Date.now();
});
schema.post('find', function(result) {
console.log(this instanceof mongoose.Query); // true
// prints returned documents
console.log('find() returned ' + JSON.stringify(result));
// prints number of milliseconds the query took
console.log('find() took ' + (Date.now() - this.start) + ' milliseconds');
});
查询中间件与文档中间件的细微差别但很重要:在文档中间件中,this
指的是正在更新的文档。在查询中间件中,mongoose 不一定具有对正在更新的文档的引用,因此 this
指的是查询对象而不是正在更新的文档。
例如,如果您想在每个 updateOne()
调用中添加 updatedAt
时间戳,您将使用以下预处理钩子。
schema.pre('updateOne', function() {
this.set({ updatedAt: new Date() });
});
您无法在 pre('updateOne')
或 pre('findOneAndUpdate')
查询中间件中访问正在更新的文档。如果您需要访问要更新的文档,您需要对文档执行显式查询。
schema.pre('findOneAndUpdate', async function() {
const docToUpdate = await this.model.findOne(this.getQuery());
console.log(docToUpdate); // The document that `findOneAndUpdate()` will modify
});
但是,如果您定义 pre('updateOne')
文档中间件,this
将是正在更新的文档。这是因为 pre('updateOne')
文档中间件挂钩到 Document#updateOne()
而不是 Query#updateOne()
。
schema.pre('updateOne', { document: true, query: false }, function() {
console.log('Updating');
});
const Model = mongoose.model('Test', schema);
const doc = new Model();
await doc.updateOne({ $set: { name: 'test' } }); // Prints "Updating"
// Doesn't print "Updating", because `Query#updateOne()` doesn't fire
// document middleware.
await Model.updateOne({}, { $set: { name: 'test' } });
错误处理中间件
中间件执行通常在中间件第一次使用错误调用 next()
时停止。但是,有一种称为“错误处理中间件”的特殊后处理中间件,它专门在发生错误时执行。错误处理中间件对于报告错误和使错误消息更易读很有用。
错误处理中间件定义为接受一个额外参数的中间件:作为函数的第一个参数发生的“错误”。错误处理中间件随后可以根据您的需要转换错误。
const schema = new Schema({
name: {
type: String,
// Will trigger a MongoServerError with code 11000 when
// you save a duplicate
unique: true
}
});
// Handler **must** take 3 parameters: the error that occurred, the document
// in question, and the `next()` function
schema.post('save', function(error, doc, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next();
}
});
// Will trigger the `post('save')` error handler
Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);
错误处理中间件也适用于查询中间件。您还可以定义一个后处理 update()
钩子,该钩子将捕获 MongoDB 重复键错误。
// The same E11000 error can occur when you call `updateOne()`
// This function **must** take 4 parameters.
schema.post('updateOne', function(passRawResult, error, res, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `updateOne()` call will still error out.
}
});
const people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
await Person.create(people);
// Throws "There was a duplicate key error"
await Person.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
错误处理中间件可以转换错误,但它无法删除错误。即使您如上所示在没有错误的情况下调用 next()
,函数调用仍会出错。
聚合钩子
您还可以为 Model.aggregate()
函数 定义钩子。在聚合中间件函数中,this
指的是 Mongoose Aggregate
对象。例如,假设您在 Customer
模型上实现软删除,方法是添加一个 isDeleted
属性。要确保 aggregate()
调用只查看未软删除的客户,您可以使用以下中间件将 $match
阶段 添加到每个 聚合管道 的开头。
customerSchema.pre('aggregate', function() {
// Add a $match state to the beginning of each pipeline.
this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });
});
Aggregate#pipeline()
函数 使您可以访问 Mongoose 将发送到 MongoDB 服务器的 MongoDB 聚合管道。它对于从中间件将阶段添加到管道的开头很有用。
同步钩子
某些 Mongoose 钩子是同步的,这意味着它们**不支持**返回 Promise 或接收 next()
回调的函数。目前,只有 init
钩子是同步的,因为init()
函数 是同步的。以下是用 pre 和 post init 钩子的示例。
[require:post init hooks.*success]
要在 init 钩子中报告错误,您必须抛出一个**同步**错误。与所有其他中间件不同,init 中间件**不会**处理 Promise 拒绝。
[require:post init hooks.*error]
下一步
现在我们已经介绍了中间件,让我们来看看 Mongoose 使用其查询填充 助手模拟 JOIN 的方法。