Middleware中间件
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.中间件是在模式级别指定的,对于编写插件非常有用。
Types of Middleware中间件的类型- Pre
Errors in Pre HooksPre挂钩中的错误- Post
Asynchronous Post Hooks异步Post挂钩Define Middleware Before Compiling Models在编译模型之前定义中间件Save/Validate Hooks保存/验证挂钩Naming Conflicts命名冲突Notes on findAndUpdate() and Query Middleware关于findAndUpdate()
和查询中间件的注意事项Error Handling Middleware错误处理中间件Aggregation Hooks聚合挂钩Synchronous Hooks同步挂钩
Types of Middleware中间件的类型
Mongoose has 4 types of middleware: document middleware, model middleware, aggregate middleware, and query middleware.Mongoose有4种类型的中间件:文档中间件、模型中间件、聚合中间件和查询中间件。
Document middleware is supported for the following document functions. 以下文档功能支持文档中间件。In Mongoose, a document is an instance of a 在Mongoose中,文档是Model
class. Model
类的一个实例。In document middleware functions, 在文档中间件功能中,this
refers to the document. this
指的是文档。To access the model, use 若要访问模型,请使用this.constructor
.this.constructor
。
Query middleware is supported for the following Query functions. 以下查询功能支持查询中间件。Query middleware executes when you call 当您对Query对象调用exec()
or then()
on a Query object, or await
on a Query object. exec()
或then()
,或对Query对象执行await
时,查询中间件就会执行。In query middleware functions, 在查询中间件功能中,this
refers to the query.this
指的是查询。
- count
- countDocuments
- deleteMany
- deleteOne
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndRemove
- findOneAndReplace
- findOneAndUpdate
- remove
- replaceOne
- update
- updateOne
- updateMany
- validate
Aggregate middleware is for 聚合中间件用于MyModel.aggregate()
. MyModel.aggregate()
。Aggregate middleware executes when you call 当您对聚合对象调用exec()
on an aggregate object. exec()
时,会执行聚合中间件。In aggregate middleware, 在聚合中间件中,this
refers to the aggregation object.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. Model
类上的静态函数,文档中间件挂接到模型类上的方法。In model middleware functions, 在模型中间件功能中,this
refers to the model.this
引用的是模型。
Here are the possible strings that can be passed to 以下是可以传递给pre()
pre()
的可能字符串
- aggregate
- count
- countDocuments
- deleteOne
- deleteMany
- estimatedDocumentCount
- find
- findOne
- findOneAndDelete
- findOneAndRemove
- findOneAndReplace
- findOneAndUpdate
- init
- insertMany
- remove
- 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.下面将更详细地描述前挂钩和后挂钩的工作方式。
Note: If you specify schema.pre('remove')
, Mongoose will register this middleware for doc.remove() by default. If you want your middleware to run on Query.remove() use schema.pre('remove', { query: true, document: false }, fn).
Note: Unlike schema.pre('remove')
, Mongoose registers updateOne
and deleteOne
middleware on Query#updateOne()
and Query#deleteOne()
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
or deleteOne
middleware as document middleware, use schema.pre('updateOne', { document: true, query: false })
.
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
Pre middleware functions are executed one after another, when each middleware calls 当每个中间件调用next
.next
时,预中间件功能会一个接一个地执行。
const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
// do stuff
next();
});
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, in Node.js >= 7.6.0:
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
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预弯钩中的错误
If any pre hook errors out, mongoose will not execute subsequent middleware or the hooked function. 如果出现任何预挂接错误,mongoose将不会执行后续的中间件或挂接的函数。Mongoose will instead pass an error to the callback and/or reject the returned promise. Mongoose将向回调传递一个错误和/或拒绝返回的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()`,则该参数被认为是一个错误。
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您也可以在`async`函数中抛出错误
throw new Error('something went wrong');
});
// later...
// Changes will not be persisted to MongoDB because a pre hook errored out更改将不会持久化到MongoDB,因为预挂接出错
myDoc.save(function(err) {
console.log(err.message); // something went wrong
});
Calling 多次调用next()
multiple times is a no-op. next()
是不允许的。If you call 如果调用next()
with an error err1
and then throw an error err2
, mongoose will report err1
.next()
时出现错误err1
,然后抛出错误err2
,则mongoose将报告err1
。
Post middlewarePost中间件
post middleware are executed after the hooked method and all of its 中间件是在钩子方法及其所有pre
middleware have completed.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('remove', function(doc) {
console.log('%s has been removed', doc._id);
});
Asynchronous Post Hooks异步柱挂钩
If your post hook function takes at least 2 parameters, mongoose will assume the second parameter is a 如果post-hook函数至少需要2个参数,mongoose将假设第二个参数是next()
function that you will call to trigger the next middleware in the sequence.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();
});
Define Middleware Before Compiling Models在编译模型之前定义中间件
Calling 编译模型后调用pre()
or post()
after compiling a model does not work in Mongoose in general. pre()
或post()
在Mongoose中通常不起作用。For example, the below 例如,下面的pre('save')
middleware will not fire.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();
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();
As a consequence, be careful about exporting Mongoose models from the same file that you define your schema. 因此,要小心从定义模式的同一文件中导出Mongoose模型。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保存/验证挂钩
The save()
function triggers validate()
hooks, because mongoose has a built-in pre('save')
hook that calls validate()
. save()
函数触发validate()
钩子,因为猫鼬有一个内置的pre('save')
钩子,可以调用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');
});
Naming Conflicts命名冲突
Mongoose has both query and document hooks for Mongoose同时具有remove()
.remove()
的查询和文档挂钩。
schema.pre('remove', function() { console.log('Removing!'); });
// Prints "Removing!"
doc.remove();
// Does **not** print "Removing!". Query middleware for `remove` is not
// executed by default.
Model.remove();
You can pass options to Schema.pre() and Schema.post() to switch whether Mongoose calls your remove()
hook for Document.remove() or Model.remove(). Note here that you need to set both 请注意,您需要在传递的对象中设置document
and query
properties in the passed object:document
和query
属性:
// Only document middleware仅限文档中间件
schema.pre('remove', { document: true, query: false }, function() {
console.log('Removing doc!');
});
// Only query middleware. This will get called when you do `Model.remove()`
// but not `doc.remove()`.
schema.pre('remove', { query: true, document: false }, function() {
console.log('Removing!');
});
Notes on findAndUpdate() and Query Middleware关于findAndUpdate()
和查询中间件的注意事项
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.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');
});
Query middleware differs from document middleware in a subtle but important way: in document middleware, 查询中间件与文档中间件的区别在于一个微妙但重要的方面:在文档中间件中,this
refers to the document being updated. this
指的是正在更新的文档。In query middleware, mongoose doesn't necessarily have a reference to the document being updated, so 在查询中间件中,mongoose不一定引用正在更新的文档,所以this
refers to the query object rather than the document being updated.this
引用的是查询对象,而不是正在更新的文件。
For instance, if you wanted to add an 例如,如果您想在每个updatedAt
timestamp to every updateOne()
call, you would use the following pre hook.updateOne()
调用中添加一个updatedAt
时间戳,您可以使用以下预挂接。
schema.pre('updateOne', function() {
this.set({ updatedAt: new Date() });
});
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
});
However, if you define 但是,如果您定义了pre('updateOne')
document middleware, this
will be the document being updated. pre('updateOne')
文档中间件,那么this
将是正在更新的文档。That's because 这是因为pre('updateOne')
document middleware hooks into Document#updateOne() rather than Query#updateOne()
.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' } });
Error Handling Middleware错误处理中间件
New in 4.5.0
Middleware execution normally stops the first time a piece of middleware calls 中间件执行通常在中间件第一次调用next()
with an error. next()
时出错时停止。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 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' }]);
Error handling middleware also works with query middleware. 错误处理中间件也可以与查询中间件配合使用。You can also define a post 您还可以定义一个post update()
hook that will catch MongoDB duplicate key errors.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' }];
Person.create(people, function(error) {
Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
// `error.message` will be "There was a duplicate key error"
});
});
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.next()
时没有出现如上所示的错误,函数调用仍然会出错。
Aggregation Hooks
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 } } });
});
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同步挂钩
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.
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()));
To report an error in an init hook, you must throw a synchronous error. 要报告init钩子中的错误,必须抛出一个同步错误。Unlike all other middleware, init middleware does not handle promise rejections.
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'));
Next Up下一步
Now that we've covered middleware, let's take a look at Mongoose's approach to faking JOINs with its query population helper.现在我们已经介绍了中间件,让我们来看看Mongoose通过其查询填充助手伪造JOIN的方法。