Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use "q" module for refactoring mongoose code?

I'm using mongoose to insert some data into mongodb. The code looks like:

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];

        }
    });
};

You can see there are a lot of nested callbacks there, so I'm trying to use q to refactor it.

I hope the code will look like:

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

But I don't know how to do it.

like image 353
Freewind Avatar asked May 11 '12 03:05

Freewind


3 Answers

You'll want to use Q.nfcall, documented in the README and the Wiki. All Mongoose methods are Node-style. I'll also use .spread instead of manually destructuring .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
    }
);

In practice, you will rarely want to use .spread, since you usually are inserting an array that you don't know the size of. In that case the code can look more like this (here I also illustrate Q.nbind).


To compare with the original one is not quite fair, because your original has no error handling. A corrected Node-style version of the original would be like so:

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.
    }
});
like image 84
Domenic Avatar answered Sep 30 '22 19:09

Domenic


With alternative deferred promise implementation, you may do it as following:

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
});    
like image 24
Mariusz Nowak Avatar answered Sep 30 '22 18:09

Mariusz Nowak


Considering Model.save instead of Collection.insert (quite the same in our case).

You don't need to use Q, you can wrap yourself the save method and return directly a Mongoose Promise.

First create an utility method to wrap the save function, that's not very clean but something like:

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

Then you can use it instead of save to chain your promises

  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

  });

I guess that's the kind of simplicity we are looking for.. right? Of course the utility function can be implemented with better integration with Mongoose.

Let me know what you think about that.


By the way there is an issue about that exact problem in the Mongoose Github:

  • Add 'promise' return value to model save operation

I hope it's gonna be solved soon. I think it takes some times because they are thinking of switching from mpromise to Q: See here and then here.

like image 44
Yves M. Avatar answered Sep 30 '22 18:09

Yves M.