Middleware - 图1mongoose

Middleware - 图2mongoose

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 函数的时候,下一个执行。

  1. var schema = new Schema(..);
  2. schema.pre('save', function(next) {
  3. // do stuff
  4. next();
  5. });

mongoose 5.x中, 除了手动调用 next(), 你还可以返回一个promise, 甚至是 async/await

  1. schema.pre('save', function() {
  2. return doStuff().
  3. then(() => doMoreStuff());
  4. });
  5. // Or, in Node.js >= 7.6.0:
  6. schema.pre('save', async function() {
  7. await doStuff();
  8. await doMoreStuff();
  9. });

next() 不会 阻止剩余代码的运行。 你可以使用 提早 return 模式 阻止 next() 后面的代码运行。

  1. var schema = new Schema(..);
  2. schema.pre('save', function(next) {
  3. if (foo()) {
  4. console.log('calling next!');
  5. // `return next();` will make sure the rest of this function doesn't run
  6. /*return*/ next();
  7. }
  8. // Unless you comment out the `return` above, 'after next' will print
  9. console.log('after next');
  10. });

并行

并行中间件提供细粒度流控制。

  1. var schema = new Schema(..);
  2. // `true` means this is a parallel middleware. You **must** specify `true`
  3. // as the second parameter if you want to use parallel middleware.
  4. schema.pre('save', true, function(next, done) {
  5. // calling next kicks off the next middleware in parallel
  6. next();
  7. setTimeout(done, 100);
  8. });

在这个例子里,save 方法将在所有中间件都调用了 done 的时候才会执行。

使用场景

中间件对原子化模型逻辑很有帮助。这里有一些其他建议:

  • 复杂的数据校验
  • 删除依赖文档(删除用户后删除他的所有文章)
  • asynchronous defaults
  • asynchronous tasks that a certain action triggers

错误处理

如果 pre 钩子出错,mongoose 将不会执行后面的函数。 Mongoose 会向回调函数传入 err 参数, 或者 reject 返回的 promise。 这里列举几个错误处理的方法:

  1. schema.pre('save', function(next) {
  2. const err = new Error('something went wrong');
  3. // If you call `next()` with an argument, that argument is assumed to be
  4. // an error.
  5. next(err);
  6. });
  7. schema.pre('save', function() {
  8. // You can also return a promise that rejects
  9. return new Promise((resolve, reject) => {
  10. reject(new Error('something went wrong'));
  11. });
  12. });
  13. schema.pre('save', function() {
  14. // You can also throw a synchronous error
  15. throw new Error('something went wrong');
  16. });
  17. schema.pre('save', async function() {
  18. await Promise.resolve();
  19. // You can also throw an error in an `async` function
  20. throw new Error('something went wrong');
  21. });
  22. // later...
  23. // Changes will not be persisted to MongoDB because a pre hook errored out
  24. myDoc.save(function(err) {
  25. console.log(err.message); // something went wrong
  26. });

多次调用 next() 是无效的。如果你调用 next() 带有错误参数 err1, 然后你再抛一个 err2,mongoose 只会传递 err1

Post 中间件

post 中间件在方法执行之后 调用,这个时候每个 pre 中间件都已经完成。

  1. schema.post('init', function(doc) {
  2. console.log('%s has been initialized from the db', doc._id);
  3. });
  4. schema.post('validate', function(doc) {
  5. console.log('%s has been validated (but not saved yet)', doc._id);
  6. });
  7. schema.post('save', function(doc) {
  8. console.log('%s has been saved', doc._id);
  9. });
  10. schema.post('remove', function(doc) {
  11. console.log('%s has been removed', doc._id);
  12. });

异步 Post 钩子

如果你给回调函数传入两个参数,mongoose 会认为第二个参数是 next() 函数,你可以通过 next 触发下一个中间件

  1. // Takes 2 parameters: this is an asynchronous post hook
  2. schema.post('save', function(doc, next) {
  3. setTimeout(function() {
  4. console.log('post1');
  5. // Kick off the second post hook
  6. next();
  7. }, 10);
  8. });
  9. // Will not execute until the first middleware calls `next()`
  10. schema.post('save', function(doc, next) {
  11. console.log('post2');
  12. next();
  13. });

Save/Validate 钩子

save() 函数触发 validate() 钩子,mongoose validate() 其实就是 pre('save') 钩子, 这意味着所有 pre('validate')post('validate') 都会在 pre('save') 钩子之前调用。

  1. schema.pre('validate', function() {
  2. console.log('this gets printed first');
  3. });
  4. schema.post('validate', function() {
  5. console.log('this gets printed second');
  6. });
  7. schema.pre('save', function() {
  8. console.log('this gets printed third');
  9. });
  10. schema.post('save', function() {
  11. console.log('this gets printed fourth');
  12. });

findAndUpdate() 与 Query 中间件使用注意

pre 和 post save() 钩子都不执行于 update()findOneAndUpdate() 等情况。 你可以在此了解更多细节。 Mongoose 4.0 为这些函数制定了新钩子。

  1. schema.pre('find', function() {
  2. console.log(this instanceof mongoose.Query); // true
  3. this.start = Date.now();
  4. });
  5. schema.post('find', function(result) {
  6. console.log(this instanceof mongoose.Query); // true
  7. // prints returned documents
  8. console.log('find() returned ' + JSON.stringify(result));
  9. // prints number of milliseconds the query took
  10. console.log('find() took ' + (Date.now() - this.start) + ' millis');
  11. });

Query 中间件 不同于 document 中间件:document 中间件中, this 指向被更新 document,query 中间件中, this 指向 query 对象而不是被更新 document。

例如,如果你要在每次 update之前更新 updatedAt 时间戳, 你可以使用 pre 钩子。

  1. schema.pre('update', function() {
  2. this.update({},{ $set: { updatedAt: new Date() } });
  3. });

错误处理中间件

4.5.0 新增

next() 执行错误时,中间件执行立即停止。但是我们有特殊的 post 中间件技巧处理这个问题 —— 错误处理中渐渐,它可以在出错后执行你指定的代码。

错误处理中间件比普通中间件多一个 error 参数,并且 err 作为第一个参数传入。 而后错误处理中间件可以让你自由地做错误的后续处理。

  1. var schema = new Schema({
  2. name: {
  3. type: String,
  4. // Will trigger a MongoError with code 11000 when
  5. // you save a duplicate
  6. unique: true
  7. }
  8. });
  9. // 处理函数**必须**传入 3 个参数: 发生的错误
  10. // 返回的文件,以及 next 函数
  11. schema.post('save', function(error, doc, next) {
  12. if (error.name === 'MongoError' && error.code === 11000) {
  13. next(new Error('There was a duplicate key error'));
  14. } else {
  15. next(error);
  16. }
  17. });
  18. // Will trigger the `post('save')` error handler
  19. Person.create([{ name: 'Axl Rose' }, { name: 'Axl Rose' }]);

对于 query 中间件也可以使用错误处理。你可以定义一个 post update() 钩子, 它可以捕获 MongoDB 重复 key 错误。

  1. // The same E11000 error can occur when you call `update()`
  2. // This function **must** take 3 parameters. If you use the
  3. // `passRawResult` function, this function **must** take 4
  4. // parameters
  5. schema.post('update', function(error, res, next) {
  6. if (error.name === 'MongoError' && error.code === 11000) {
  7. next(new Error('There was a duplicate key error'));
  8. } else {
  9. next(error);
  10. }
  11. });
  12. var people = [{ name: 'Axl Rose' }, { name: 'Slash' }];
  13. Person.create(people, function(error) {
  14. Person.update({ name: 'Slash' }, { $set: { name: 'Axl Rose' } }, function(error) {
  15. // `error.message` will be "There was a duplicate key error"
  16. });
  17. });

下一步

我们了解了中间件,接着我们看看 Mongoose 怎么用 population 模拟 JOIN 操作。