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 操作。