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

Я использую 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

С альтернативной реализацией промиса deferred вы можете сделать это следующим образом:

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
Фривинд это правда, я проглядел. Я обновил свой пример. По сути, независимо от того, что вы делаете, вы должны инкапсулировать последовательные вызовы, чтобы иметь возможность видеть все предыдущие результаты в пределах области видимости или назначать результаты переменным из внешней области. - 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, вы можете сами обернуть save и возвращает непосредственно Обещание мангуста.

Сначала создайте служебный метод для переноса функции сохранения, это не очень чисто, но что-то вроде:

  //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 на Вопрос: см. здесь, а затем здесь.

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):
    • [ {}, ... ]
    • [ {ID_пользователя:userN._id}, ...]
    • [ {ID_пользователя:userN._id, идентификатор канала: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