mongoose + 异步流程处理

万恶的callback

在nodejs入门的章节里我们已经讲了callback约定

  1. function (err, result) {
  2. ...
  3. }

Node.js世界里,绝大部分函数都是遵守这个约定的。

举个典型的用户登录的例子吧,这是前面见过的

  1. UserSchema.statics.login = function(username, password, cb) {
  2. this.findOne({
  3. username: username
  4. }, function (err, user) {
  5. if (err || !user) {
  6. if (err)
  7. console.log(err);
  8. return cb(err, {
  9. code: -1,
  10. msg : username + ' is not exist!'
  11. });
  12. }
  13. bcrypt.compare(password, user.password, function(error, res) {
  14. if (error) {
  15. console.log(error);
  16. return cb(err, {
  17. code: -2,
  18. msg : 'password is incorrect, please check it again!'
  19. });
  20. }
  21. return cb(null, user);
  22. });
  23. });
  24. };

说明

  • login是有callback的
  • callback里遵守回调约定(err,result)

这里面findOne有一个回调,bcrypt.compare有一个回调,最后解决过通过login的回调传值回去,这还只是一个简单的逻辑,如果更复杂呢?

说callback是万恶的,其实一点也不冤枉,它其实是一种约定,但它却被滥用,导致给Node.js带来了长久以来很多人对Node.js的误解,但本身它只是一种形式,并不是最佳实践,所以持这种态度来看待Node.js是不公平的。

内置Promises:mpromise

Mongoose 异步操作,像 .save() 和 queries,返回 Promises/A+ conformant promises. This means that you can do things like MyModel.findOne({}).then() and yield MyModel.findOne({}).exec() (if you’re using co).
这就是说你可以做一些像MyModel.findOne({}).then() 和 yield MyModel.findOne({}).exec()(如果你在用co)

为了向后兼容,mongoose 4默认返回mpromise promise。

  1. var gnr = new Band({
  2. name: "Guns N' Roses",
  3. members: ['Axl', 'Slash']
  4. });
  5. var promise = gnr.save();
  6. assert.ok(promise instanceof require('mpromise'));
  7. promise.then(function (doc) {
  8. assert.equal(doc.name, "Guns N' Roses");
  9. });

https://github.com/aheckmann/mpromise

Queries are not promises

mongoose查询不是promise。可是它有 yield 和 async/await 的 .then() 方法。如果你需要健全的promise,用.exec()方法。

  1. var query = Band.findOne({name: "Guns N' Roses"});
  2. assert.ok(!(query instanceof require('mpromise')));
  3. // A query is not a fully-fledged promise, but it does have a `.then()`.
  4. query.then(function (doc) {
  5. // use doc
  6. });
  7. // `.exec()` gives you a fully-fledged promise
  8. var promise = query.exec();
  9. assert.ok(promise instanceof require('mpromise'));
  10. promise.then(function (doc) {
  11. // use doc
  12. });

使用其他 Promises 库

在mongoose 4.1.0更新,在mpromise满足基本使用的情况下,高级用户可能想插入他们喜爱的ES6风格的Promise库如bluebird,或只是使用原生的ES6 promise。设置mongoose.Promise 给你喜欢的ES6风格的promise构造函数然后mongoose会使用它。

  1. var query = Band.findOne({name: "Guns N' Roses"});
  2. // Use native promises
  3. mongoose.Promise = global.Promise;
  4. assert.equal(query.exec().constructor, global.Promise);
  5. // Use bluebird
  6. mongoose.Promise = require('bluebird');
  7. assert.equal(query.exec().constructor, require('bluebird'));
  8. // Use q. Note that you **must** use `require('q').Promise`.
  9. mongoose.Promise = require('q').Promise;
  10. assert.ok(query.exec() instanceof require('q').makePromise);

MongoDB驱动的promise

mongoose.Promise属性设置mongoose使用promise。可是,这不影响底层MongoDB驱动。如果你使用底层驱动,例如Mondel.collection.db.insert(),你需要做点额外工作来改变底层promise库。注意,下面的代码假设mongoose >= 4.4.4。

  1. var uri = 'mongodb://localhost:27017/mongoose_test';
  2. // Use bluebird
  3. var options = { promiseLibrary: require('bluebird') };
  4. var db = mongoose.createConnection(uri, options);
  5. Band = db.model('band-promises', { name: String });
  6. db.on('open', function() {
  7. assert.equal(Band.collection.findOne().constructor, require('bluebird'));
  8. });

bluebird promisifyAll

promisifyAll是bluebird提供的一个极其方便的api,可以把某个对象上的所有方法都变成返回promise对象方法,在Promise/A+规范里,只要返回promise对象就可以thenable组合完成业务逻辑组合。这是异步流程里比较好的方式。

如果Model里的方法都能返回Promise对象,那么这些方法就可以理解是乐高积木,在我们写业务逻辑时候,组合这些小模块就好了。

promisifyAll会把对象上的方法copy一份返回Promise对象的以“方法名Async”为名称的方法

原理

举例db/promisifyAll.js

  1. var UserModel = {
  2. create: function () {
  3. },
  4. retrieve: function () {
  5. },
  6. update: function () {
  7. },
  8. delete: function () {
  9. }
  10. }
  11. var Promise = require("bluebird");
  12. // Promisify
  13. Promise.promisifyAll(UserModel);
  14. console.dir(UserModel)

执行

  1. $ node promisifyAll.js
  2. {
  3. create: [Function],
  4. retrieve: [Function],
  5. update: [Function],
  6. delete: [Function],
  7. createAsync: [Function],
  8. retrieveAsync: [Function],
  9. updateAsync: [Function],
  10. deleteAsync: [Function]
  11. }

很明显,create被copy成了createAsync方法,其他亦然。也就是说[原来的方法]变成了[原来的方法Async]

下面看一下createAsync方法是否是返回Promise对象,按照co源码里判断promise的写法

  1. function isPromise(obj) {
  2. return 'function' == typeof obj.then;
  3. }
  4. var is_promise = isPromise(UserModel.createAsync());
  5. console.log(is_promise)

返回是true,也就是说createAsync方法是返回的Promise对象。

具体实例

  1. var mongoose = require('mongoose');
  2. var Promise = require("bluebird");
  3. // 定义Schema
  4. UserSchema = new mongoose.Schema({
  5. username: {// 真实姓名
  6. type: String,
  7. required: true
  8. },
  9. password: { // 密码
  10. type: String,
  11. required: true
  12. }
  13. });
  14. // 定义Model
  15. var UserModel = mongoose.model('User', UserSchema);
  16. // Promisify
  17. Promise.promisifyAll(UserModel);
  18. Promise.promisifyAll(UserModel.prototype);
  19. // 暴露接口
  20. module.exports = UserModel;

步骤说明

步骤1:引入bluebird

  1. var Promise = require("bluebird");

步骤2:给某个对象应用promisifyAll

  1. // Promisify
  2. Promise.promisifyAll(UserModel);
  3. Promise.promisifyAll(UserModel.prototype);

测试代码

db/promisify/test.js

  1. import test from 'ava';
  2. // 1、引入`mongoose connect`
  3. require('../connect');
  4. // 2、引入`User` Model
  5. const User = require('../user/promisify/user');
  6. // 3、定义`user` Entity
  7. const user = new User({
  8. username: 'i5ting',
  9. password: '0123456789'
  10. });
  11. test.cb('#thenable for default', t => {
  12. user.save().then( (user) => {
  13. // console.log(user)
  14. t.pass()
  15. t.end()
  16. }).catch((err) => {
  17. t.ifError(err);
  18. t.fail();
  19. t.end()
  20. })
  21. });
  22. test.cb('#thenable for bluebird promisifyAll', t => {
  23. user.saveAsync().then( (user) => {
  24. // console.log(user)
  25. t.pass()
  26. t.end()
  27. }).catch((err) => {
  28. t.ifError(err);
  29. t.fail();
  30. t.end()
  31. })
  32. });
  33. test.cb('#thenable for bluebird Async methods', t => {
  34. user.saveAsync().then( (u) => {
  35. return User.findByIdAndUpdateAsync( u._id, {'username' : 'aaaa'})
  36. }).then((updated_result) => {
  37. // console.log(updated_result)
  38. return User.findOneAsync({'username' : 'aaaa'});
  39. }).then((find_user) => {
  40. // console.log(find_user)
  41. t.pass()
  42. t.end()
  43. }).catch((err) => {
  44. t.ifError(err);
  45. t.fail();
  46. t.end()
  47. })
  48. });

总结:东西虽好,但不要滥用

凡是奇技淫巧都有特定场景的应用场景,虽好,但不要滥用。如果转换非常多,会有性能问题。但是在某些场景,比如模型操作上,还是非常方便、高效的。

generator in co

http://mongoosejs.com/docs/harmony.html

  1. co(function*() {
  2. var error;
  3. var schema = new Schema({
  4. description: {type: String, required: true}
  5. });
  6. var Breakfast = db.model('breakfast', schema, getCollectionName());
  7. var goodBreakfast = new Breakfast({description: 'eggs & bacon'});
  8. try {
  9. yield goodBreakfast.save();
  10. } catch (e) {
  11. error = e;
  12. }
  13. assert.ifError(error);
  14. var result;
  15. try {
  16. result = yield Breakfast.findOne().exec();
  17. } catch (e) {
  18. error = e;
  19. }
  20. assert.ifError(error);
  21. assert.equal('eggs & bacon', result.description);
  22. // Should cause a validation error because `description` is required
  23. var badBreakfast = new Breakfast({});
  24. try {
  25. yield badBreakfast.save();
  26. } catch (e) {
  27. error = e;
  28. }
  29. assert.ok(error);
  30. assert.ok(error instanceof ValidationError);
  31. done();
  32. })();

async/await

支持yield,其实就等于支持async/await了,但目前性能测试还不够好,所以暂时还不推荐使用。