中间件
中间件(也称为 pre 和 post hook)是在异步函数执行期间传递控制权的函数。 中间件是在结构级别指定的,对于编写 plugins 很有用。
- 中间件类型
- 预
- Pre Hook 中的错误
- 帖子
- 异步后钩子
- 在编译模型之前定义中间件
- Save/Validate Hooks
- 访问中间件中的参数
- 命名冲突
- 关于 findAndUpdate() 和查询中间件的注释
- 错误处理中间件
- 聚合钩子
- 同步钩子
中间件类型
Mongoose 有 4 种类型的中间件: 文档中间件、模型中间件、聚合中间件和查询中间件。
文档中间件支持以下文档功能。
在 Mongoose 中,文档是 Model
类的实例。
在文档中间件函数中,this
指的是文档。 要访问模型,请使用 this.constructor
。
查询中间件支持以下查询功能。
当你在查询对象上调用 exec()
或 then()
或在查询对象上调用 await
时,查询中间件就会执行。
在查询中间件函数中,this
指的是查询。
- count
- countDocuments
- deleteMany
- deleteOne
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndReplace
- findOneAndUpdate
- remove
- replaceOne
- update
- updateOne
- updateMany
- validate
聚合中间件适用于 MyModel.aggregate()
。
当你对聚合对象调用 exec()
时,聚合中间件就会执行。
在聚合中间件中,this
指的是 聚合对象。
模型中间件支持以下模型功能。
不要混淆模型中间件和文档中间件: 模型中间件钩子到 Model
类上的静态函数,文档中间件钩子到 Model
类上的方法。
在模型中间件函数中,this
指的是模型。
以下是可以传递给 pre()
的可能字符串
- 总计的
- 数数
- countDocuments
- deleteOne
- deleteMany
- 估计文档数
- 寻找
- findOne
- 查找并删除
- 查找并替换
- 查找并更新
- 在里面
- insertMany
- 删除
- replaceOne
- 保存
- 更新
- updateOne
- updateMany
- 证实
所有中间件类型都支持前置和后置钩子。 下面更详细地描述了前置钩子和后置钩子的工作原理。
注意: 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()
确实 not 触发了 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
.Pre 中间件函数时,Pre 中间件函数会依次执行。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
在 Mongoose 5.x 中,你可以使用返回 Promise 的函数,而不是手动调用 next()
。 特别是,你可以使用 async/await
。
schema.pre('save', function() {
return doStuff().
then(() => doMoreStuff());
});
// Or, in Node.js >= 7.6.0:
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
如果你使用 next()
,则 next()
调用会使 not 停止执行中间件函数中的其余代码。 使用 早期的 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');
});
用例
中间件对于原子化模型逻辑很有用。 以下是一些其他想法:
- 复杂的验证
- 删除相关文档(删除用户会删除他们的所有博客文章)
- 异步默认值
- 某个动作触发的异步任务
Pre Hook 中的错误
如果任何 pre hook 出错,mongoose 将不会执行后续的中间件或 hooked 函数。 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()
是无操作的。 如果调用 next()
时出现错误 err1
,然后抛出错误 err2
,mongoose 将报告 err1
。
后置中间件
post 中间件在 hooked 方法及其所有 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);
});
异步后钩子
如果你的 post hook 函数至少需要 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();
});
在编译模型之前定义中间件
一般来说,在 编译模型 之后调用 pre()
或 post()
not 可以在 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();
这意味着你必须添加所有中间件和 plugins before 调用 mongoose.model()
。
下面的脚本将打印出 "来自预保存的你好":
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 模型时要小心。 如果你选择使用此结构,则必须在模型文件上定义 全局插件 before 调用 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 Hooks
save()
函数触发 validate()
钩子,因为 mongoose 有一个内置的 pre('save')
钩子调用 validate()
。 这意味着所有 pre('validate')
和 post('validate')
钩子都会被称为 before 任何 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 提供了 2 种方法来获取有关触发中间件的函数调用的信息。
对于查询中间件,我们建议使用 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 将第一个参数传递给 save()
,作为 pre('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 `remove` is not executed by default
await doc.deleteOne();
// Prints "Removing!"
Model.remove();
你可以将选项传递给 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.remove()`
// but not `doc.remove()`.
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()
之前和之后的钩子是 not 在 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
引用 query 对象而不是正在更新的文档。
例如,如果你想为每个 updateOne()
调用添加 updatedAt
时间戳,则可以使用以下 pre hook。
schema.pre('updateOne', function() {
this.set({ updatedAt: new Date() });
});
你 cannot 访问正在 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()
并出现错误时,中间件执行通常会停止。 但是,有一种特殊的后置中间件,称为 "错误处理中间件",它会在发生错误时专门执行。 错误处理中间件对于报告错误并使错误消息更具可读性非常有用。
错误处理中间件被定义为带有一个额外参数的中间件: 作为函数第一个参数出现的 'error'。 然后,错误处理中间件可以根据需要转换错误。
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' }]);
错误处理中间件也可以与查询中间件一起使用。 你还可以定义一个 post update()
钩子来捕获 MongoDB 重复键错误。
// The same E11000 error can occur when you call `update()`
// This function **must** take 3 parameters. If you use the
// `passRawResult` function, this function **must** take 4
// parameters
schema.post('update', function(error, res, next) {
if (error.name === 'MongoServerError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `update()` 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.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });
错误处理中间件可以转换错误,但不能消除错误。 即使你调用 next()
时没有出现如上所示的错误,该函数调用仍然会出错。
聚合钩子
你还可以为 Model.aggregate()
功能 定义钩子。
聚合中间件函数中,this
指的是 Mongoose Aggregate
对象。
例如,假设你通过添加 isDeleted
属性在 Customer
模型上实现软删除。 为了确保 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()
回调的 not 函数。 目前,只有 init
钩子是同步的,因为 init()
功能 是同步的。 下面是使用 pre 和 post init 钩子的示例。
const schema = new Schema({ title: String, loadedAt: Date });
schema.pre('init', pojo => {
assert.equal(pojo.constructor.name, 'Object'); // Plain object before init
});
const now = new Date();
schema.post('init', doc => {
assert.ok(doc instanceof mongoose.Document); // Mongoose doc after init
doc.loadedAt = now;
});
const Test = db.model('Test', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
then(doc => assert.equal(doc.loadedAt.valueOf(), now.valueOf()));
要报告 init 钩子中的错误,你必须抛出 synchronous 错误。 与所有其他中间件不同,init 中间件 not 处理 promise 拒绝。
const schema = new Schema({ title: String });
const swallowedError = new Error('will not show');
// init hooks do **not** handle async errors or any sort of async behavior
schema.pre('init', () => Promise.reject(swallowedError));
schema.post('init', () => { throw Error('will show'); });
const Test = db.model('Test', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
catch(error => assert.equal(error.message, 'will show'));
下一步
现在我们已经介绍了中间件,让我们看一下 Mongoose 使用其查询 population 辅助程序伪造 JOIN 的方法。