Как да използвам q модул за рефакторинг на mongoose код?

Използвам 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();

Но не знам как да го направя.


person Freewind    schedule 11.05.2012    source източник


Отговори (5)


Ще искате да използвате Q.nfcall, документиран в 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, тъй като обикновено вмъквате масив, чийто размер не знаете. В такъв случай кодът може да изглежда повече така (тук също илюстрирам Q.nbind).


Да се ​​сравнява с оригинала не е съвсем честно, защото вашият оригинал няма обработка на грешки. Коригираната версия на оригинала в стил Node би била така:

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.
    }
});
person Domenic    schedule 11.05.2012
comment
Благодаря, но... не го намирам за по-прост от оригиналния :( - person Freewind; 11.05.2012

С алтернативно изпълнение на отложено обещание можете да го направите по следния начин:

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
});    
person Mariusz Nowak    schedule 11.05.2012
comment
conn.collection('articles').pinsert([{userId:user1._id: не мога да получа user1 тук - person Freewind; 11.05.2012
comment
Freewind това е вярно, пренебрегнах това. Актуализирах моя пример. По принцип, без значение какво правите, трябва да капсулирате последователни извиквания, за да можете да видите всички предишни резултати в обхвата или да присвоите резултати на променливи от външния обхват. - person Mariusz Nowak; 11.05.2012
comment
Благодаря ти много. Всъщност вашето решение ми харесва много повече. но... тъй като въпросът е use module q, не мога да приема отговора ви тук. - person Freewind; 11.05.2012
comment
няма проблем :) Просто исках да покажа, че има и алтернативни решения - person Mariusz Nowak; 11.05.2012

Имайки предвид Model.save вместо Collection.insert (съвсем същото в нашия случай).

Не е необходимо да използвате Q, можете да опаковате метод за запазване и директно връщане на 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;
  }

След това можете да го използвате вместо запазване, за да верижите обещанията си

  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: Вижте тук и след това тук.

person Yves M.    schedule 09.12.2013
comment
Мисля, че добра точка за добавяне на помощна функция е прототип на модел mongoose.Model.prototype.saveInPromise = function() { ... }; - person farincz; 25.03.2014

Две години по-късно този въпрос просто изскочи в моя RSS клиент ...

Нещата се подобриха донякъде от май 2012 г. и сега може да изберем да решим този въпрос по различен начин. По-конкретно, общността на Javascript стана „наясно с намаляването“ след решението да се включат Array.prototype.reduce (и други методи на Array) в ECMAScript5. Array.prototype.reduce винаги беше (и все още е) наличен като полифил, но беше малко оценен от много от нас по това време. Разбира се, онези, които изпреварват кривата, могат да възразят по този въпрос.

Проблемът, поставен във въпроса, изглежда формулиран, със следните правила:

  • The objects in the array passed as the first param to conn.collection(table).insert() build as follows (where N corresponds to the object's index in an array):
    • [ {}, ... ]
    • [ {userId:userN._id}, ... ]
    • [ {userId:userN._id, channelId:channelN._id}, ... ]
  • имената на таблиците (по ред) са: users, channels, articles.
  • съответните свойства на обекта са: user, channel, article (т.е. имената на таблиците без множественото число 's').

Общ модел от тази статия от 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
});

Това работи добре, защото всички таблици във въпроса имат редовно множествено число (потребител => потребители, канал => канали). Ако някое от тях е неправилно (напр. стимул => стимули, дете => деца), тогава ще трябва да преосмислим - (и вероятно да приложим хеш за търсене). Във всеки случай адаптацията би била доста тривиална.

person Roamer-1888    schedule 14.04.2014

Днес имаме и mongoose-q. Плъгин към mongoose, който ви дава неща като execQ и saveQ, които връщат Q обещания.

person kmpm    schedule 17.09.2014