中间件
¥Middleware
中间件(也称为 pre 和 post hook)是在异步函数执行期间传递控制权的函数。中间件是在结构级别指定的,对于编写 plugins 很有用。
¥Middleware (also called pre and post hooks) are functions which are passed control during execution of asynchronous functions. Middleware is specified on the schema level and is useful for writing plugins.
- 中间件类型
- 预
- 预钩子中的错误
- 后
- 异步后钩子
- 在编译模型之前定义中间件
- 保存/验证钩子
- 访问中间件中的参数
- 命名冲突
- 关于 findAndUpdate() 和查询中间件的注释
- 错误处理中间件
- 聚合钩子
- 同步钩子
中间件类型
¥Types of Middleware
Mongoose 有 4 种类型的中间件:文档中间件、模型中间件、聚合中间件和查询中间件。
¥Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware.
文档中间件支持以下文档功能。在 Mongoose 中,文档是 Model
类的实例。在文档中间件函数中,this
指的是文档。要访问模型,请使用 this.constructor
。
¥Document middleware is supported for the following document functions.
In Mongoose, a document is an instance of a Model
class.
In document middleware functions, this
refers to the document. To access the model, use this.constructor
.
-
¥save
init(注意:init hooks 是 synchronous)
¥init (note: init hooks are synchronous)
查询中间件支持以下查询功能。当你在查询对象上调用 exec()
或 then()
或在查询对象上调用 await
时,查询中间件就会执行。在查询中间件函数中,this
指的是查询。
¥Query middleware is supported for the following Query functions.
Query middleware executes when you call exec()
or then()
on a Query object, or await
on a Query object.
In query middleware functions, this
refers to the query.
聚合中间件适用于 MyModel.aggregate()
。当你对聚合对象调用 exec()
时,聚合中间件就会执行。在聚合中间件中,this
指的是 聚合对象。
¥Aggregate middleware is for MyModel.aggregate()
.
Aggregate middleware executes when you call exec()
on an aggregate object.
In aggregate middleware, this
refers to the aggregation object.
模型中间件支持以下模型功能。不要混淆模型中间件和文档中间件:模型中间件钩子到 Model
类上的静态函数,文档中间件钩子到 Model
类上的方法。在模型中间件函数中,this
指的是模型。
¥Model middleware is supported for the following model functions.
Don't confuse model middleware and document middleware: model middleware hooks into static functions on a Model
class, document middleware hooks into methods on a Model
class.
In model middleware functions, this
refers to the model.
以下是可以传递给 pre()
的可能字符串
¥Here are the possible strings that can be passed to pre()
aggregate
bulkWrite
count
countDocuments
createCollection
deleteOne
deleteMany
估计文档数
¥estimatedDocumentCount
find
findOne
查找并删除
¥findOneAndDelete
查找并替换
¥findOneAndReplace
查找并更新
¥findOneAndUpdate
init
insertMany
replaceOne
保存
¥save
update
updateOne
updateMany
validate
所有中间件类型都支持前置和后置钩子。下面更详细地描述了前置钩子和后置钩子的工作原理。
¥All middleware types support pre and post hooks. How pre and post hooks work is described in more detail below.
注意:Mongoose 默认在 Query.prototype.updateOne()
上注册 updateOne
中间件。这意味着 doc.updateOne()
和 Model.updateOne()
都会触发 updateOne
钩子,但 this
指的是查询,而不是文档。要将 updateOne
中间件注册为文档中间件,请使用 schema.pre('updateOne', { document: true, query: false })
。
¥Note: Mongoose registers updateOne
middleware on Query.prototype.updateOne()
by default.
This means that both doc.updateOne()
and Model.updateOne()
trigger updateOne
hooks, but this
refers to a query, not a document.
To register updateOne
middleware as document middleware, use 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 })
。
¥Note: Like updateOne
, Mongoose registers deleteOne
middleware on Query.prototype.deleteOne
by default.
That means that Model.deleteOne()
will trigger deleteOne
hooks, and this
will refer to a query.
However, doc.deleteOne()
does not fire deleteOne
query middleware for legacy reasons.
To register deleteOne
middleware as document middleware, use schema.pre('deleteOne', { document: true, query: false })
.
注意:create()
函数触发 save()
钩子。
¥Note: The create()
function fires save()
hooks.
注意:查询中间件不在子文档上执行。
¥Note: Query middlewares are not executed on subdocuments.
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
});
预
¥Pre
当每个中间件调用 next
.Pre 中间件函数时,Pre 中间件函数会依次执行。
¥Pre middleware functions are executed one after another, when each
middleware calls next
.
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
在 Mongoose 5.x 中,你可以使用返回 Promise 的函数,而不是手动调用 next()
。特别是,你可以使用 async/await
。
¥In mongoose 5.x, instead of calling next()
manually, you can use a
function that returns a promise. In particular, you can use 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()
时运行。
¥If you use next()
, the next()
call does not stop the rest of the code in your middleware function from executing. Use
the early return
pattern
to prevent the rest of your middleware function from running when you call 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');
});
用例
¥Use Cases
中间件对于原子化模型逻辑很有用。以下是一些其他想法:
¥Middleware are useful for atomizing model logic. Here are some other ideas:
复杂的验证
¥complex validation
删除相关文档(删除用户会删除他们的所有博客文章)
¥removing dependent documents (removing a user removes all their blogposts)
异步默认值
¥asynchronous defaults
某个动作触发的异步任务
¥asynchronous tasks that a certain action triggers
预钩子中的错误
¥Errors in Pre Hooks
如果任何 pre hook 出错,mongoose 将不会执行后续的中间件或 hooked 函数。Mongoose 会将错误传递给回调和/或拒绝返回的 Promise。中间件有几种报告错误的方法:
¥If any pre hook errors out, mongoose will not execute subsequent middleware or the hooked function. Mongoose will instead pass an error to the callback and/or reject the returned promise. There are several ways to report an error in middleware:
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
。
¥Calling next()
multiple times is a no-op. If you call next()
with an
error err1
and then throw an error err2
, mongoose will report err1
.
后置中间件
¥Post middleware
post 中间件在 hooked 方法及其所有 pre
中间件完成后执行。
¥post middleware are executed after
the hooked method and all of its pre
middleware have completed.
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);
});
异步后钩子
¥Asynchronous Post Hooks
如果你的 post hook 函数至少需要 2 个参数,mongoose 将假定第二个参数是 next()
函数,你将调用该函数来触发序列中的下一个中间件。
¥If your post hook function takes at least 2 parameters, mongoose will assume the second parameter is a next()
function that you will call to trigger the next middleware in the sequence.
// 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 解决。
¥You can also pass an async function to post()
.
If you pass an async function that takes at least 2 parameters, you are still responsible for calling next()
.
However, you can also pass in an async function that takes less than 2 parameters, and Mongoose will wait for the promise to resolve.
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();
});
在编译模型之前定义中间件
¥Define Middleware Before Compiling Models
一般来说,在 编译模型 之后调用 pre()
或 post()
在 Mongoose 中不起作用。例如,下面的 pre('save')
中间件将不会触发。
¥Calling pre()
or post()
after compiling a model
does not work in Mongoose in general. For example, the below pre('save')
middleware will not fire.
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()
之前添加所有中间件和 plugins。下面的脚本将打印出 "来自预保存的你好":
¥This means that you must add all middleware and plugins
before calling mongoose.model()
.
The below script will print out "Hello from 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()
之前定义 全局插件。
¥As a consequence, be careful about exporting Mongoose models from the same
file that you define your schema. If you choose to use this pattern, you
must define global plugins
before calling require()
on your model file.
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')
钩子都会在任何 pre('save')
钩子之前被调用。
¥The save()
function triggers validate()
hooks, because mongoose
has a built-in pre('save')
hook that calls validate()
. This means
that all pre('validate')
and post('validate')
hooks get called
before any pre('save')
hooks.
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');
});
访问中间件中的参数
¥Accessing Parameters in Middleware
Mongoose 提供了 2 种方法来获取有关触发中间件的函数调用的信息。对于查询中间件,我们建议使用 this
,它将是 Mongoose 查询实例。
¥Mongoose provides 2 ways to get information about the function call that triggered the middleware.
For query middleware, we recommend using this
, which will be a Mongoose Query instance.
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()
的所有选项。
¥For document middleware, like pre('save')
, Mongoose passes the 1st parameter to save()
as the 2nd argument to your pre('save')
callback.
You should use the 2nd argument to get access to the save()
call's options
, because Mongoose documents don't store all the options you can pass to 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 });
命名冲突
¥Naming Conflicts
Mongoose 具有 deleteOne()
的查询和文档钩子。
¥Mongoose has both query and document hooks for 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
属性:
¥You can pass options to Schema.pre()
and Schema.post()
to switch whether
Mongoose calls your deleteOne()
hook for Document.prototype.deleteOne()
or Query.prototype.deleteOne()
. Note here that you need to set both document
and query
properties in the passed object:
// 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
。
¥Mongoose also has both query and document hooks for validate()
.
Unlike deleteOne
and updateOne
, validate
middleware applies to Document.prototype.validate
by default.
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()
和查询中间件的注释
¥Notes on findAndUpdate()
and Query Middleware
save()
之前和之后的钩子不会在 update()
、findOneAndUpdate()
等上执行。你可以在 这个 GitHub 问题 中看到更详细的讨论。Mongoose 4.0 为这些函数引入了不同的钩子。
¥Pre and post save()
hooks are not executed on update()
,
findOneAndUpdate()
, etc. You can see a more detailed discussion why in
this GitHub issue.
Mongoose 4.0 introduced distinct hooks for these functions.
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 middleware differs from document middleware in a subtle but
important way: in document middleware, this
refers to the document
being updated. In query middleware, mongoose doesn't necessarily have
a reference to the document being updated, so this
refers to the
query object rather than the document being updated.
例如,如果你想为每个 updateOne()
调用添加 updatedAt
时间戳,则可以使用以下 pre hook。
¥For instance, if you wanted to add an updatedAt
timestamp to every
updateOne()
call, you would use the following pre hook.
schema.pre('updateOne', function() {
this.set({ updatedAt: new Date() });
});
你无法访问 pre('updateOne')
或 pre('findOneAndUpdate')
查询中间件中正在更新的文档。如果需要访问将要更新的文档,则需要对该文档执行显式查询。
¥You cannot access the document being updated in pre('updateOne')
or
pre('findOneAndUpdate')
query middleware. If you need to access the document
that will be updated, you need to execute an explicit query for the document.
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()
。
¥However, if you define pre('updateOne')
document middleware,
this
will be the document being updated. That's because pre('updateOne')
document middleware hooks into Document#updateOne()
rather than 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' } });
错误处理中间件
¥Error Handling Middleware
当中间件第一次调用 next()
并出现错误时,中间件执行通常会停止。但是,有一种特殊的后置中间件,称为 "错误处理中间件",它会在发生错误时专门执行。错误处理中间件对于报告错误并使错误消息更具可读性非常有用。
¥Middleware execution normally stops the first time a piece of middleware
calls next()
with an error. However, there is a special kind of post
middleware called "error handling middleware" that executes specifically
when an error occurs. Error handling middleware is useful for reporting
errors and making error messages more readable.
错误处理中间件被定义为带有一个额外参数的中间件:作为函数第一个参数出现的 'error'。然后,错误处理中间件可以根据需要转换错误。
¥Error handling middleware is defined as middleware that takes one extra parameter: the 'error' that occurred as the first parameter to the function. Error handling middleware can then transform the error however you want.
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 重复键错误。
¥Error handling middleware also works with query middleware. You can
also define a post update()
hook that will catch MongoDB duplicate key
errors.
// 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()
时没有出现如上所示的错误,该函数调用仍然会出错。
¥Error handling middleware can transform an error, but it can't remove the
error. Even if you call next()
with no error as shown above, the
function call will still error out.
聚合钩子
¥Aggregation Hooks
你还可以为 Model.aggregate()
function 定义钩子。聚合中间件函数中,this
指的是 Mongoose Aggregate
对象。例如,假设你通过添加 isDeleted
属性在 Customer
模型上实现软删除。为了确保 aggregate()
调用仅查看未软删除的客户,你可以使用以下中间件将 $match
阶段 添加到每个 聚合管道 的开头。
¥You can also define hooks for the Model.aggregate()
function.
In aggregation middleware functions, this
refers to the Mongoose Aggregate
object.
For example, suppose you're implementing soft deletes on a Customer
model
by adding an isDeleted
property. To make sure aggregate()
calls only look
at customers that aren't soft deleted, you can use the below middleware to
add a $match
stage to the beginning
of each aggregation pipeline.
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 聚合管道。它对于从中间件将阶段添加到管道的开头非常有用。
¥The Aggregate#pipeline()
function
lets you access the MongoDB aggregation pipeline that Mongoose will send to
the MongoDB server. It is useful for adding stages to the beginning of the
pipeline from middleware.
同步钩子
¥Synchronous Hooks
某些 Mongoose 钩子是同步的,这意味着它们不支持返回 Promise 或接收 next()
回调的函数。目前,只有 init
钩子是同步的,因为 init()
功能 是同步的。下面是使用 pre 和 post init 钩子的示例。
¥Certain Mongoose hooks are synchronous, which means they do not support
functions that return promises or receive a next()
callback. Currently,
only init
hooks are synchronous, because the init()
function
is synchronous. Below is an example of using pre and post init hooks.
[require:post init hooks.*success]
要报告 init 钩子中的错误,你必须抛出同步错误。与所有其他中间件不同,init 中间件不处理 promise 拒绝。
¥To report an error in an init hook, you must throw a synchronous error. Unlike all other middleware, init middleware does not handle promise rejections.
[require:post init hooks.*error]
下一步
¥Next Up
现在我们已经介绍了中间件,让我们看一下 Mongoose 使用其查询 population 辅助程序伪造 JOIN 的方法。
¥Now that we've covered middleware, let's take a look at Mongoose's approach to faking JOINs with its query population helper.