1. 中间件
中间件也称为“前置”(pre)和“后置”(post)钩子,是在执行异步功能期间传递控制的函数。中间件在模式(Schema)级别指定,对编写插件很有用。Mongoose中有4种类型的中间件:文档(document)中间件,模型(model)中间件,聚合(aggregate)中间件和查询(query )中间件。
文档中间件
文档中间件支持在以下文档方法中使用。在文档中间件中,this指向文档:
模型中间件
文档中间件支持在以下模型方法中使用。在模型中间件中,this指向当前模型:
聚合中间件
聚合中间件用于MyModel.aggregate()。当在聚合对象上调用exec()时,将执行聚合中间件。在聚合中间件中,this指向聚合对象。
查询中间件
查询中间件用于以下Model和Query方法中。在查询中间件中,this指向当前查询。
所有类型的中间件都支持前/后置钩子。下面将更详细地介绍前/后钩子的工作原理。
注意:如果指定了schema.pre('remove'),Mongoose默认将会为doc.remove()注册此中间件。如果想要该中间件也在上运行,请设置schema.pre('remove', { query: true, document: false }, fn)
另外,create()方法会触发save()钩子。
2. 前置(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(),则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');
});
用例
中间件对于原子模型逻辑很有用。以下是一些建议用法:
- 复杂的验证
- 删除依赖文档(如:删除用户则删除所有他的博客帖子)
- 异步默认值
- 某个操作触发的异步任务
错误处理
如果任何前置钩子输出错误,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。
3. 后置(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);
});
4. 异步Post钩子
如果你的post钩子函数至少需要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();
});
5. 保存/验证钩子
save()方法会触发validate()钩子,因为Mongoose有个名为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');
});
6. 命名冲突
Mongoose有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();
可以通过向Schema.pre()和Schema.post()传入设置选项,以切换Mongoose是否为Document.remove()或Model.remove调用remove()钩子:
// Only document middleware
schema.pre('remove', { document: true } 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 } function() {
console.log('Removing!');
});
7. 关于findAndUpdate()和Query中间件的说明
update(), findOneAndUpdate()等方法上不会执行Pre和Postsave()钩子,可以在此GitHub Isuue中查看更详细的原因讨论。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');
});
查询中间件与文档中间件的区别在于:在文档中间件中,this指向正在更新的文档;而在查询中间件中,mongoose不一定具有对正在更新的文档的引用,所以this指向的是查询对象而不是正在更新的文档。
例如,如果要为每个update()调用添加updatedAt的时间戳,则可以使用以下pre钩子:
schema.pre('update', function() {
this.update({},{ $set: { updatedAt: new Date() } });
});
8. 错误处理中间件
添加于 4.5.0
中间件执行通常会在中间件调用next()第一次出现错误时停止。但是,有一种特殊的后置中间件称为“错误处理中间件”,它会在发生错误时执行。错误处理中间件对于报告错误和使错误消息更具可读性非常有用。
错误处理中间件被定义为带有一个额外参数的中间件:作为函数的第一个参数出现的“错误”。然后,错误处理中间件可以根据需要转换错误。
var schema = new Schema({
name: {
type: String,
// Will trigger a MongoError 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 === 'MongoError' && 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' }]);
如下所示,错误处理中间件也适用于查询中间件。还可以定义一个postupdate()挂钩,它将捕获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 === 'MongoError' && error.code === 11000) {
next(new Error('There was a duplicate key error'));
} else {
next(); // The `update()` call will still error out.
}
});
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"
});
});
错误处理中间件可以转换错误,但不能删除错误。即使你如上所示调用next()没有错误,函数调用仍然会出错。
9. 同步钩子
有些Mongoose钩了是同步的,这意味着它们不支持返回promise或接收next()回调的函数。目前,只有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('TestPostInitMiddleware', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
then(doc => assert.equal(doc.loadedAt.valueOf(), now.valueOf()));
在init钩子中报告错误时,必须抛出一个同步错误。与所有其他中间件不同,init中间件不处理reject状态的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('PostInitBook', schema);
return Test.create({ title: 'Casino Royale' }).
then(doc => Test.findById(doc)).
catch(error => assert.equal(error.message, 'will show'));
下一步
现在我们已经介绍了中间件,接下来让我们来看看Mongoose用其查询populate帮助器实现类似SQL中的JOIN查询的方法。
变更记录
- [2018-11-25] 基于Mongoose官方文档
v5.3.12首次发布
