Mongoose中文文档-指南之中间件(Middleware)

 2018年11月25日    615     声明


  1. 中间件
  2. 前置(pre)中间件
  3. 后置(post)中间件
  4. 异步Post钩子
  5. 保存/验证钩子
  6. 命名冲突
  7. 关于findAndUpdate()和Query中间件的说明
  8. 错误处理中间件
  9. 同步钩子


1. 中间件

中间件也称为“前置”(pre)和“后置”(post)钩子,是在执行异步功能期间传递控制的函数。中间件在模式(Schema)级别指定,对编写插件很有用。Mongoose中有4种类型的中间件:文档(document)中间件,模型(model)中间件,聚合(aggregate)中间件和查询(query )中间件。

文档中间件

文档中间件支持在以下文档方法中使用。在文档中间件中,this指向文档:

模型中间件

文档中间件支持在以下模型方法中使用。在模型中间件中,this指向当前模型:

聚合中间件

聚合中间件用于MyModel.aggregate()。当在聚合对象上调用exec()时,将执行聚合中间件。在聚合中间件中,this指向聚合对象

查询中间件

查询中间件用于以下ModelQuery方法中。在查询中间件中,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.xnext()不用手工调用,可以使用返回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首次发布