中间件

中间件(也称为预处理和后处理 _钩子_)是在执行异步函数期间传递控制的函数。中间件在模式级别指定,对于编写 插件 很有用。

中间件类型

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

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

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

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

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

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

  • aggregate
  • bulkWrite
  • count
  • countDocuments
  • createCollection
  • deleteOne
  • deleteMany
  • estimatedDocumentCount
  • find
  • findOne
  • findOneAndDelete
  • findOneAndReplace
  • findOneAndUpdate
  • init
  • insertMany
  • replaceOne
  • save
  • update
  • updateOne
  • updateMany
  • validate

所有中间件类型都支持预处理和后处理钩子。预处理和后处理钩子的工作原理将在下面详细介绍。

注意: 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() 不会触发 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 时。

const 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, using async functions
schema.pre('save', async function() {
  await doStuff();
  await doMoreStuff();
});

如果您使用 next()next() 调用 不会阻止您中间件函数中的其余代码执行。使用 早期 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');
});

用例

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

  • 复杂验证
  • 删除依赖文档(删除用户会删除其所有博客文章)
  • 异步默认值
  • 特定操作触发的异步任务

预处理钩子中的错误

如果任何预处理钩子出错,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

后处理中间件

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('deleteOne', function(doc) {
  console.log('%s has been deleted', doc._id);
});

异步后处理钩子

如果您的后处理钩子函数至少接受 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();
});

您还可以将异步函数传递给 post()。如果您传递一个至少接受 2 个参数的异步函数,您仍然需要负责调用 next()。但是,您也可以传递一个接受不到 2 个参数的异步函数,Mongoose 将等待 Promise 解析。

schema.post('save', async function(doc) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('post1');
  // If less than 2 parameters, no need to call `next()`
});

schema.post('save', async function(doc, next) {
  await new Promise(resolve => setTimeout(resolve, 1000));
  console.log('post1');
  // If there's a `next` parameter, you need to call `next()`.
  next();
});

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

编译模型 后调用 pre()post() 在 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();

这意味着您必须在调用 mongoose.model() 之前添加所有中间件和 插件。以下脚本将打印“来自 pre save 的问候”。

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 模型。如果您选择使用此模式,则必须在调用 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() 钩子,因为 mongoose 具有一个内置的 pre('save') 钩子,该钩子会调用 validate()。这意味着所有 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');
});

在中间件中访问参数

Mongoose 提供了两种方法来获取有关触发中间件的函数调用的信息。对于查询中间件,我们建议使用 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 将作为 pre('save') 回调的第二个参数,传递给 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 `deleteOne` is not executed by default
await doc.deleteOne();

// Prints "Removing!"
await Model.deleteOne();

您可以将选项传递给 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.deleteOne()`
// but not `doc.deleteOne()`.
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() 钩子不会update()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 指的是查询对象而不是正在更新的文档。

例如,如果您想在每个 updateOne() 调用中添加 updatedAt 时间戳,您将使用以下预处理钩子。

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

无法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() 时停止。但是,有一种称为“错误处理中间件”的特殊后处理中间件,它专门在发生错误时执行。错误处理中间件对于报告错误和使错误消息更易读很有用。

错误处理中间件定义为接受一个额外参数的中间件:作为函数的第一个参数发生的“错误”。错误处理中间件随后可以根据您的需要转换错误。

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

错误处理中间件也适用于查询中间件。您还可以定义一个后处理 update() 钩子,该钩子将捕获 MongoDB 重复键错误。

// The same E11000 error can occur when you call `updateOne()`
// This function **must** take 4 parameters.

schema.post('updateOne', function(passRawResult, error, res, next) {
  if (error.name === 'MongoServerError' && error.code === 11000) {
    next(new Error('There was a duplicate key error'));
  } else {
    next(); // The `updateOne()` 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.updateOne({ name: 'Slash' }, { $set: { name: 'Axl Rose' } });

错误处理中间件可以转换错误,但它无法删除错误。即使您如上所示在没有错误的情况下调用 next(),函数调用仍会出错。

聚合钩子

您还可以为 Model.aggregate() 函数 定义钩子。在聚合中间件函数中,this 指的是 Mongoose Aggregate 对象。例如,假设您在 Customer 模型上实现软删除,方法是添加一个 isDeleted 属性。要确保 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() 回调的函数。目前,只有 init 钩子是同步的,因为init() 函数 是同步的。以下是用 pre 和 post init 钩子的示例。

[require:post init hooks.*success]

要在 init 钩子中报告错误,您必须抛出一个**同步**错误。与所有其他中间件不同,init 中间件**不会**处理 Promise 拒绝。

[require:post init hooks.*error]

下一步

现在我们已经介绍了中间件,让我们来看看 Mongoose 使用其查询填充 助手模拟 JOIN 的方法。