Middleware
中间件 (pre 和 post 钩子) 是在异步函数执行时函数传入的控制函数。
Middleware is specified
on the schema 级别,在写插件的时候很有用。
Mongoose 4.x 有四种中间件: document 中间件,model 中间件,aggregate 中间件,和 query 中间件。
对于 document 中间件, this
指向当前 document。
Document 中间件支持以下 document 操作:
对于 query 中间件,this
指向当前 query。
Query 中间件支持以下 Model 和 Query 操作:
Aggregate 中间件作用于 MyModel.aggregate()
,
它会在你对 aggregate 对象调用 exec()
时执行。
对于 aggregate 中间件,this
指向当前aggregation 对象。
对于 model 中间件,this
指向当前 model。
Model 中间件支持以下 Model 操作:
所有中间件支持 pre 和 post 钩子, 下面将详细解释这两个钩子的细节。
注意: Query 是没有 remove()
钩子的,只有 document 有,
如果你设定了 'remove' 钩子,他将会在你调用 myDoc.remove()
(而不是 MyModel.remove()
)时触发。
注意: create()
函数会触发 save()
钩子.
Pre
pre
钩子分串行和并行两种。
串行
串行中间件一个接一个地执行。具体来说,
上一个中间件调用 next
函数的时候,下一个执行。
var 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, in Node.js >= 7.6.0:
schema.pre('save', async function() {
await doStuff();
await doMoreStuff();
});
next()
不会 阻止剩余代码的运行。
你可以使用 提早
return
模式
阻止 next()
后面的代码运行。
var 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');
});
并行
并行中间件提供细粒度流控制。
var schema = new Schema(..);
// `true` means this is a parallel middleware. You **must** specify `true`
// as the second parameter if you want to use parallel middleware.
schema.pre('save', true, function(next, done) {
// calling next kicks off the next middleware in parallel
next();
setTimeout(done, 100);
});
在这个例子里,save
方法将在所有中间件都调用了 done
的时候才会执行。
使用场景
中间件对原子化模型逻辑很有帮助。这里有一些其他建议:
- 复杂的数据校验
- 删除依赖文档(删除用户后删除他的所有文章)
- asynchronous defaults
- asynchronous tasks that a certain action triggers
错误处理
如果 pre 钩子出错,mongoose 将不会执行后面的函数。 Mongoose 会向回调函数传入 err 参数, 或者 reject 返回的 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 中间件
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('remove', function(doc) {
console.log('%s has been removed', doc._id);
});
异步 Post 钩子
如果你给回调函数传入两个参数,mongoose
会认为第二个参数是 next()
函数,你可以通过 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();
});
Save/Validate 钩子
save()
函数触发 validate()
钩子,mongoose
validate()
其实就是 pre('save')
钩子,
这意味着所有 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');
});
findAndUpdate() 与 Query 中间件使用注意
pre 和 post save()
钩子都不执行于 update()
,findOneAndUpdate()
等情况。
你可以在此了解更多细节。
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) + ' millis');
});
Query 中间件 不同于 document 中间件:document 中间件中,
this
指向被更新 document,query 中间件中,
this
指向 query 对象而不是被更新 document。
例如,如果你要在每次 update之前更新 updatedAt
时间戳,
你可以使用 pre
钩子。
schema.pre('update', function() {
this.update({},{ $set: { updatedAt: new Date() } });
});
错误处理中间件
4.5.0 新增
next()
执行错误时,中间件执行立即停止。但是我们有特殊的 post 中间件技巧处理这个问题 ——
错误处理中渐渐,它可以在出错后执行你指定的代码。
错误处理中间件比普通中间件多一个 error
参数,并且 err
作为第一个参数传入。
而后错误处理中间件可以让你自由地做错误的后续处理。
var schema = new Schema({
name: {
type: String,
// Will trigger a MongoError with code 11000 when
// you save a duplicate
unique: true
}
});
// 处理函数**必须**传入 3 个参数: 发生的错误
// 返回的文件,以及 next 函数
schema.post('save', function(error, doc, next) {
if (error.name === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(error);
}
});
// Will trigger the `post('save')` error handler
Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);
对于 query 中间件也可以使用错误处理。你可以定义一个 post update()
钩子,
它可以捕获 MongoDB 重复 key 错误。
// 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 === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(error);
}
});
var 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"
});
});
下一步
我们了解了中间件,接着我们看看 Mongoose 怎么用 population 模拟 JOIN 操作。