中间件

中间件(也称为 pre 和 post hook)是在异步函数执行期间传递控制权的函数。 中间件是在结构级别指定的,对于编写 plugins 很有用。

中间件类型

Mongoose 有 4 种类型的中间件: 文档中间件、模型中间件、聚合中间件和查询中间件。

文档中间件支持以下文档功能。 在 Mongoose 中,文档是 Model 类的实例。 在文档中间件函数中,this 指的是文档。 要访问模型,请使用 this.constructor

查询中间件支持以下查询功能。 当你在查询对象上调用 exec()then() 或在查询对象上调用 await 时,查询中间件就会执行。 在查询中间件函数中,this 指的是查询。

聚合中间件适用于 MyModel.aggregate()。 当你对聚合对象调用 exec() 时,聚合中间件就会执行。 在聚合中间件中,this 指的是 聚合对象

模型中间件支持以下模型功能。 不要混淆模型中间件和文档中间件: 模型中间件钩子到 Model 类上的静态函数,文档中间件钩子到 Model 类上的方法。 在模型中间件函数中,this 指的是模型。

以下是可以传递给 pre() 的可能字符串

  • 总计的
  • 数数
  • countDocuments
  • deleteOne
  • deleteMany
  • 估计文档数
  • 寻找
  • findOne
  • 查找并删除
  • 查找并替换
  • 查找并更新
  • 在里面
  • insertMany
  • 删除
  • replaceOne
  • 保存
  • 更新
  • updateOne
  • updateMany
  • 证实

所有中间件类型都支持前置和后置钩子。 下面更详细地描述了前置钩子和后置钩子的工作原理。

注意: Mongoose 默认在 Query.prototype.updateOne() 上注册 updateOne 中间件。 这意味着 doc.updateOne()Model.updateOne() 都会触发 updateOne 钩子,但 this 指的是查询,而不是文档。 要将 updateOne 中间件注册为文档中间件,请使用 schema.pre('updateOne', { document: true, query: false })

注意:updateOne 一样,Mongoose 默认在 Query.prototype.deleteOne 上注册 deleteOne 中间件。 这意味着 Model.deleteOne() 将触发 deleteOne 钩子,this 将引用查询。 然而,由于遗留原因,doc.deleteOne() 确实 not 触发了 deleteOne 查询中间件。 要将 deleteOne 中间件注册为文档中间件,请使用 schema.pre('deleteOne', { document: true, query: false })

注意: create() 函数触发 save() 钩子。

注意: 查询中间件不在子文档上执行。

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
});

当每个中间件调用 next.Pre 中间件函数时,Pre 中间件函数会依次执行。

const schema = new Schema({ /* ... */ });
schema.pre('save', function(next) {
  // do stuff
  next();
});

Mongoose 5.x 中,你可以使用返回 Promise 的函数,而不是手动调用 next()。 特别是,你可以使用 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() 调用会使 not 停止执行中间件函数中的其余代码。 使用 早期的 return 模式 可以阻止中间件函数的其余部分在调用 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');
});

用例

中间件对于原子化模型逻辑很有用。 以下是一些其他想法:

  • 复杂的验证
  • 删除相关文档(删除用户会删除他们的所有博客文章)
  • 异步默认值
  • 某个动作触发的异步任务

Pre Hook 中的错误

如果任何 pre hook 出错,mongoose 将不会执行后续的中间件或 hooked 函数。 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() 是无操作的。 如果调用 next() 时出现错误 err1,然后抛出错误 err2,mongoose 将报告 err1

后置中间件

post 中间件在 hooked 方法及其所有 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('deleteOne', function(doc) {
  console.log('%s has been deleted', doc._id);
});

异步后钩子

如果你的 post hook 函数至少需要 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();
});

在编译模型之前定义中间件

一般来说,在 编译模型 之后调用 pre()post() not 可以在 Mongoose 中工作。 例如,下面的 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();

这意味着你必须添加所有中间件和 plugins before 调用 mongoose.model()。 下面的脚本将打印出 "来自预保存的你好":

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();

因此,从定义结构的同一文件中导出 Mongoose 模型时要小心。 如果你选择使用此结构,则必须在模型文件上定义 全局插件 before 调用 require()

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

save() 函数触发 validate() 钩子,因为 mongoose 有一个内置的 pre('save') 钩子调用 validate()。 这意味着所有 pre('validate')post('validate') 钩子都会被称为 before 任何 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');
});

访问中间件中的参数

Mongoose 提供了 2 种方法来获取有关触发中间件的函数调用的信息。 对于查询中间件,我们建议使用 this,它将是 Mongoose 查询实例

const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('findOneAndUpdate', function() {
  console.log(this.getFilter()); // { name: 'John' }
  console.log(this.getUpdate()); // { age: 30 }
});
const User = mongoose.model('User', userSchema);

await User.findOneAndUpdate({ name: 'John' }, { $set: { age: 30 } });

对于文档中间件,如 pre('save'),Mongoose 将第一个参数传递给 save(),作为 pre('save') 回调的第二个参数。 你应该使用第二个参数来访问 save() 调用的 options,因为 Mongoose 文档不存储可以传递给 save() 的所有选项。

const userSchema = new Schema({ name: String, age: Number });
userSchema.pre('save', function(next, options) {
  options.validateModifiedOnly; // true

  // Remember to call `next()` unless you're using an async function or returning a promise
  next();
});
const User = mongoose.model('User', userSchema);

const doc = new User({ name: 'John', age: 30 });
await doc.save({ validateModifiedOnly: true });

命名冲突

Mongoose 具有 deleteOne() 的查询和文档钩子。

schema.pre('deleteOne', function() { console.log('Removing!'); });

// Does **not** print "Removing!". Document middleware for `remove` is not executed by default
await doc.deleteOne();

// Prints "Removing!"
Model.remove();

你可以将选项传递给 Schema.pre()Schema.post() 来切换 Mongoose 是否为 Document.prototype.deleteOne()Query.prototype.deleteOne() 调用 deleteOne() 钩子。 这里请注意,你需要在传递的对象中设置 documentquery 属性:

// Only document middleware
schema.pre('deleteOne', { document: true, query: false }, function() {
  console.log('Deleting doc!');
});

// Only query middleware. This will get called when you do `Model.remove()`
// but not `doc.remove()`.
schema.pre('deleteOne', { query: true, document: false }, function() {
  console.log('Deleting!');
});

Mongoose 还具有 validate() 的查询和文档钩子。 与 deleteOneupdateOne 不同,validate 中间件默认适用于 Document.prototype.validate

const schema = new mongoose.Schema({ name: String });
schema.pre('validate', function() {
  console.log('Document validate');
});
schema.pre('validate', { query: true, document: false }, function() {
  console.log('Query validate');
});
const Test = mongoose.model('Test', schema);

const doc = new Test({ name: 'foo' });

// Prints "Document validate"
await doc.validate();

// Prints "Query validate"
await Test.find().validate();

关于 findAndUpdate() 和查询中间件的注释

save() 之前和之后的钩子是 notupdate()findOneAndUpdate() 等上执行的。你可以在 这个 GitHub 问题 中看到更详细的讨论。 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');
});

查询中间件与文档中间件有一个微妙但重要的区别: 在文档中间件中,this 指的是正在更新的文档。 在查询中间件中,mongoose 不一定具有对正在更新的文档的引用,因此 this 引用 query 对象而不是正在更新的文档。

例如,如果你想为每个 updateOne() 调用添加 updatedAt 时间戳,则可以使用以下 pre hook。

schema.pre('updateOne', function() {
  this.set({ updatedAt: new Date() });
});

cannot 访问正在 pre('updateOne')pre('findOneAndUpdate') 查询中间件中更新的文档。 如果需要访问将要更新的文档,则需要对该文档执行显式查询。

schema.pre('findOneAndUpdate', async function() {
  const docToUpdate = await this.model.findOne(this.getQuery());
  console.log(docToUpdate); // The document that `findOneAndUpdate()` will modify
});

但是,如果你定义 pre('updateOne') 文档中间件,则 this 将是正在更新的文档。 这是因为 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' } });

错误处理中间件

当中间件第一次调用 next() 并出现错误时,中间件执行通常会停止。 但是,有一种特殊的后置中间件,称为 "错误处理中间件",它会在发生错误时专门执行。 错误处理中间件对于报告错误并使错误消息更具可读性非常有用。

错误处理中间件被定义为带有一个额外参数的中间件: 作为函数第一个参数出现的 'error'。 然后,错误处理中间件可以根据需要转换错误。

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' }]);

错误处理中间件也可以与查询中间件一起使用。 你还可以定义一个 post 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' }];
await Person.create(people);

// Throws "There was a duplicate key error"
await Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });

错误处理中间件可以转换错误,但不能消除错误。 即使你调用 next() 时没有出现如上所示的错误,该函数调用仍然会出错。

聚合钩子

你还可以为 Model.aggregate() 功能 定义钩子。 聚合中间件函数中,this 指的是 Mongoose Aggregate 对象。 例如,假设你通过添加 isDeleted 属性在 Customer 模型上实现软删除。 为了确保 aggregate() 调用仅查看未软删除的客户,你可以使用以下中间件将 $match 阶段 添加到每个 聚合管道 的开头。

customerSchema.pre('aggregate', function() {
  // Add a $match state to the beginning of each pipeline.
  this.pipeline().unshift({ $match: { isDeleted: { $ne: true } } });
});

Aggregate#pipeline() 功能 允许你访问 Mongoose 将发送到 MongoDB 服务器的 MongoDB 聚合管道。 它对于从中间件将阶段添加到管道的开头非常有用。

同步钩子

某些 Mongoose 钩子是同步的,这意味着它们支持返回 Promise 或接收 next() 回调的 not 函数。 目前,只有 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('Test', schema);

return Test.create({ title: 'Casino Royale' }).
  then(doc => Test.findById(doc)).
  then(doc => assert.equal(doc.loadedAt.valueOf(), now.valueOf()));

要报告 init 钩子中的错误,你必须抛出 synchronous 错误。 与所有其他中间件不同,init 中间件 not 处理 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('Test', schema);

return Test.create({ title: 'Casino Royale' }).
  then(doc => Test.findById(doc)).
  catch(error => assert.equal(error.message, 'will show'));

下一步

现在我们已经介绍了中间件,让我们看一下 Mongoose 使用其查询 population 辅助程序伪造 JOIN 的方法。