【问题标题】:How to use "q" module for refactoring mongoose code?如何使用“q”模块重构猫鼬代码?
【发布时间】:2012-05-19 16:19:12
【问题描述】:

我正在使用 mongoose 将一些数据插入 mongodb。代码如下:

var mongoose = require('mongoose');
mongoose.connect('mongo://localhost/test');
var conn = mongoose.connection;

// insert users
conn.collection('users').insert([{/*user1*/},{/*user2*/}], function(err, docs) {
    var user1 = docs[0], user2 = docs[1];

    // insert channels
    conn.collection('channels').insert([{userId:user1._id},{userId:user2._id}], function(err, docs) {
        var channel1 = docs[0], channel2 = docs[1];

        // insert articles
        conn.collection('articles').insert([{userId:user1._id,channelId:channel1._id},{}], function(err, docs) {
            var article1 = docs[0], article2 = docs[1];

        }
    });
};

你可以看到那里有很多嵌套的回调,所以我尝试使用q 来重构它。

我希望代码看起来像:

Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
    // Do something with value4
}, function (error) {
    // Handle any error from step1 through step4
})
.end();

但我不知道该怎么做。

【问题讨论】:

    标签: javascript asynchronous mongoose promise q


    【解决方案1】:

    您需要使用Q.nfcall、记录在案的in the README 和Wiki。所有的 Mongoose 方法都是 Node 风格的。我还将使用.spread,而不是手动解构.then

    var mongoose = require('mongoose');
    mongoose.connect('mongo://localhost/test');
    var conn = mongoose.connection;
    
    var users = conn.collection('users');
    var channels = conn.collection('channels');
    var articles = conn.collection('articles');
    
    function getInsertedArticles() {
        return Q.nfcall(users.insert.bind(users), [{/*user1*/},{/*user2*/}]).spread(function (user1, user2) {
            return Q.nfcall(channels.insert.bind(channels), [{userId:user1._id},{userId:user2._id}]).spread(function (channel1, channel2) {
                return Q.nfcall(articles.insert.bind(articles), [{userId:user1._id,channelId:channel1._id},{}]);
            });
        })
    }
    
    getInsertedArticles()
        .spread(function (article1, article2) {
            // you only get here if all three of the above steps succeeded
        })
        .fail(function (error) {
            // you get here if any of the above three steps failed
        }
    );
    

    在实践中,您很少需要使用.spread,因为您通常会插入一个不知道其大小的数组。在这种情况下,代码可以看起来更像like this(这里我也说明了Q.nbind)。


    与原始版本比较不太公平,因为您的原始版本没有错误处理。原版的更正节点样式版本如下所示:

    var mongoose = require('mongoose');
    mongoose.connect('mongo://localhost/test');
    var conn = mongoose.connection;
    
    function getInsertedArticles(cb) {
        // insert users
        conn.collection('users').insert([{/*user1*/},{/*user2*/}], function(err, docs) {
            if (err) {
                cb(err);
                return;
            }
    
            var user1 = docs[0], user2 = docs[1];
    
            // insert channels
            conn.collection('channels').insert([{userId:user1._id},{userId:user2._id}], function(err, docs) {
                if (err) {
                    cb(err);
                    return;
                }
    
                var channel1 = docs[0], channel2 = docs[1];
    
                // insert articles
                conn.collection('articles').insert([{userId:user1._id,channelId:channel1._id},{}], function(err, docs) {
                    if (err) {
                        cb(err);
                        return;
                    }
    
                    var article1 = docs[0], article2 = docs[1];
    
                    cb(null, [article1, article2]);
                }
            });
        };
    }
    
    getInsertedArticles(function (err, articles) {
        if (err) {
            // you get here if any of the three steps failed.
            // `articles` is `undefined`.
        } else {
            // you get here if all three succeeded.
            // `err` is null.
        }
    });
    

    【讨论】:

    • 谢谢,但是...我不觉得它比原来的简单:(
    【解决方案2】:

    使用替代deferred promise 实现,您可以按以下方式进行:

    var mongoose = require('mongoose');
    mongoose.connect('mongo://localhost/test');
    var conn = mongoose.connection;
    
    // Setup 'pinsert', promise version of 'insert' method
    var promisify = require('deferred').promisify
    mongoose.Collection.prototype.pinsert = promisify(mongoose.Collection.prototype.insert);
    
    var user1, user2;
    // insert users
    conn.collection('users').pinsert([{/*user1*/},{/*user2*/}])
    // insert channels
    .then(function (users) {
      user1 = users[0]; user2 = users[1];
      return conn.collection('channels').pinsert([{userId:user1._id},{userId:user2._id}]);
    })
    // insert articles
    .match(function (channel1, channel2) {
      return conn.collection('articles').pinsert([{userId:user1._id,channelId:channel1._id},{}]);
    })
    .done(function (articles) {
      // Do something with articles
    }, function (err) {
       // Handle any error that might have occurred on the way
    });    
    

    【讨论】:

    • conn.collection('articles').pinsert([{userId:user1._id: 在这里找不到user1
    • Freewind 是真的,我忽略了这一点。我更新了我的例子。基本上无论您做什么,您都必须封装后续调用,以便能够在范围内查看所有先前的结果,或者将结果分配给外部范围的变量。
    • 非常感谢。实际上,我更喜欢您的解决方案。但是...由于问题是use module q,我不能在这里接受你的回答。
    • 没问题 :) 我只是想表明还有其他解决方案
    【解决方案3】:

    考虑Model.save 而不是Collection.insert(在我们的例子中完全一样)。

    你不需要使用Q,你可以将save方法包裹起来,直接返回一个Mongoose Promise

    首先创建一个实用方法来包装保存函数,这不是很干净,但类似于:

      //Utility function (put it in a better place)
      var saveInPromise = function (model) {
    
        var promise = new mongoose.Promise();
    
        model.save(function (err, result) {
          promise.resolve(err, result);
        });
    
        return promise;
      }
    

    然后你可以使用它而不是 save 来链接你的承诺

      var User = mongoose.model('User');
      var Channel = mongoose.model('Channel');
      var Article = mongoose.model('Article');
    
      //Step 1
      var user = new User({data: 'value'});
      saveInPromise(user).then(function () {
    
        //Step 2
        var channel = new Channel({user: user.id})
        return saveInPromise(channel);
    
      }).then(function (channel) {
    
        //Step 3
        var article = new Article({channel: channel.id})
        return saveInPromise(article);
    
      }, function (err) {
        //A single place to handle your errors
    
      });
    

    我想这就是我们正在寻找的那种简单......对吗?当然,实用功能可以通过与 Mongoose 更好的集成来实现。

    让我知道你对此的看法。


    顺便说一下,Mongoose Github 中有一个关于这个确切问题的问题:

    希望早日解决。我认为这需要一些时间,因为他们正在考虑从mpromise 切换到Q:请参阅here,然后是here

    【讨论】:

    • 我认为添加实用函数的好点是模型原型 mongoose.Model.prototype.saveInPromise = function() { ... };
    【解决方案4】:

    两年后,这个问题才出现在我的 RSS 客户端……

    自 2012 年 5 月以来,事情发生了一些变化,我们现在可能会选择以不同的方式解决这个问题。更具体地说,自从决定在 ECMAScript5 中包含Array.prototype.reduce(和其他数组方法)以来,Javascript 社区已经变得“具有减少意识”。 Array.prototype.reduce 一直(现在仍然)可以作为 polyfill 使用,但当时我们中的许多人都很少欣赏它。当然,那些走在曲线前面的人可能会反对这一点。

    问题中提出的问题似乎是公式化的,规则如下:

    • 数组中的对象作为第一个参数传递给conn.collection(table).insert() 构建如下(其中N 对应于对象在数组中的索引):
      • [ {}, ... ]​​i>
      • [ {userId:userN._id}, ... ]​​i>
      • [ {userId:userN._id, channelId:channelN._id}, ... ]​​i>
    • 表名(按顺序)为:userschannelsarticles
    • 对应的对象属性为:userchannelarticle(即不带复数“s”的表名)。

    来自this article by Taoofcode) 进行串行异步调用的一般模式是:

    function workMyCollection(arr) {  
        return arr.reduce(function(promise, item) {
            return promise.then(function(result) {
                return doSomethingAsyncWithResult(item, result);
            });        
        }, q());
    }
    

    通过非常轻松的适应,可以制作此模式来编排所需的顺序:

    function cascadeInsert(tables, n) {
        /* 
        /* tables: array of unpluralisd table names
        /* n: number of users to insert.
        /* returns promise of completion|error
         */
        var ids = []; // this outer array is available to the inner functions (to be read and written to).
        for(var i=0; i<n; i++) { ids.push({}); } //initialize the ids array with n plain objects.
        return tables.reduce(function (promise, t) {
            return promise.then(function (docs) {
                for(var i=0; i<ids.length; i++) {
                    if(!docs[i]) throw (new Error(t + ": returned documents list does not match the request"));//or simply `continue;` to be error tolerant (if acceptable server-side).
                    ids[i][t+'Id'] = docs[i]._id; //progressively add properties to the `ids` objects
                }
                return insert(ids, t + 's');
            });
        }, Q());
    }
    

    最后,这是返回承诺的工作函数,insert()

    function insert(ids, t) {
        /* 
        /* ids: array of plain objects with properties as defined by the rules
        /* t: table name.
        /* returns promise of docs
         */
        var dfrd = Q.defer();
        conn.collection(t).insert(ids, function(err, docs) {
            (err) ? dfrd.reject(err) : dfrd.resolve(docs);
        });
        return dfrd.promise;
    }
    

    因此,您可以指定传递给cascadeInsert 的参数、实际表/属性名称和要插入的用户数。

    cascadeInsert( ['user', 'channel', 'article'], 2 ).then(function () {
       // you get here if everything was successful
    }).catch(function (err) {
       // you get here if anything failed
    });
    

    这很好用,因为问题中的表格都有常规复数形式(用户 => 用户,频道 => 频道)。如果其中任何一个是不规则的(例如刺激 => 刺激,孩子 => 孩子),那么我们需要重新考虑 - (并且可能实现查找哈希)。无论如何,适应将是相当微不足道的。

    【讨论】:

      【解决方案5】:

      今天我们也有mongoose-q。一个 mongoose 插件,可为您提供 execQ 和 saveQ 等返回 Q 承诺的东西。

      【讨论】:

        猜你喜欢
        • 2012-12-14
        • 2014-07-04
        • 2018-04-26
        • 1970-01-01
        • 2021-09-21
        • 1970-01-01
        • 2016-12-03
        • 1970-01-01
        • 2019-03-31
        相关资源
        最近更新 更多