Node.JS + Mongoose 回调地狱

Posted

技术标签:

【中文标题】Node.JS + Mongoose 回调地狱【英文标题】:Node.JS + Mongoose callback hell 【发布时间】:2017-04-06 13:23:01 【问题描述】:

如何将 mongoose 保存到 db 但先等待其他集合加载? 平台和流派是空的,因为“保存”功能在平台和流派加载之前运行,请帮助!

var platforms = []; //load platforms
body.release_dates.forEach(function(elem)
    Platform.findOne( id : elem.platform, function(err, result) 
            platforms.push(mongoose.Types.ObjectId(result._id));
        );
);

var genres = []; //load genre
body.genres.forEach(function(elem)
    Genre.findOne(id: elem, function(err, result)
        genres.push(mongoose.Types.ObjectId(result._id));
    )
);



//prepare to save!
var game = 
    igdb_id : body.id,
    name : body.name,
    summary : body.summary,
    storyline : body.description,
    genres : genres,
    platforms : platforms, // <- genres amd platforms empty and not wait platforms and genre array to complete
    release_date : body.original_release_date,
    cover : body.cover.cloudinary_id,
    videos: body.videos
;

var data = new Game(game);
data.save(function(err, game)
    if(err)
        res.send("500");
        return console.error(err);
    

    );

【问题讨论】:

【参考方案1】:

好的,首先,如果你不使用回调,猫鼬(至少任何最新版本)已经支持承诺......其次,下面的示例使用结合异步功能的承诺。这在 Node 7+ 中的选项标志后面,因此您应该使用 babel 进行转译。

我把 cmets 放在你应该优化你的 mongodb 调用的地方,但让逻辑尽可能接近上面,希望这对你有帮助。

关键点是......

使用 Promise,不要害怕创建额外的函数来分解逻辑 Promise.all 可用于等待并行操作完成 async 功能很棒。

代码:

// will asynchronously map your release date elements to the Platform
async function getPlatforms(releaseDates) 
  // TODO: change to single query with only needed properties
  return await Promise.all(releaseDates.map(
    elem => Platform.findOne( id: elem.platform )
  ));


// will asynchronously map your genre list into the appropriate ObjectId objects
async function getGenres(genres) 
  // TODO: change to return only single properties in a single query
  var genres = await Promise.all(genres.map(elem => Genre.findOne( id: elem )));
  return genres.map(result => mongoose.Types.ObjectId(result._id));


// asynchronous request handler (ALWAYS use a try/catch for this with express)
// not sure if current/future versions will allow for promise resulting 
// handlers/errors
async function saveGameDetails(req,res) 
  try 
    // array destructured assignment, decomposes the array
    // await will await the promise, and promise.all will take an array
    // and wrap them into a single promise.
    var [platforms, genres] = await Promise.all([
      getPlatforms(body.release_dates),
      getGenres(body.genres)
    ]);

    //prepare to save!
    var game = 
        igdb_id : body.id,
        name : body.name,
        summary : body.summary,
        storyline : body.description,
        genres : genres,
        platforms : platforms, // <- genres amd platforms empty and not wait platforms and genre array to complete
        release_date : body.original_release_date,
        cover : body.cover.cloudinary_id,
        videos: body.videos
    ;

    var data = new Game(game);
    await data.save(); //already a promise, just wait for it

    // return normal result
    res.status(200).json( success: true );
   catch(err) 
    // generic error handler, may want to have this even more generic via express
    res.status(500).json(
      error: 
        message: err.message || 'Unknown Server Error';
      
    )
  

【讨论】:

【参考方案2】:

您可以使用异步模块来完成这项工作,它非常适合执行此类任务。使用 npm 安装:npm i -S async

    var async = require ('async');

        var platforms = [];
        var genres = [];

        async.parallel([
        function(cb)
            body.release_dates.forEach(function(elem)
                Platform.findOne( id : elem.platform, function(err, result)
                    cb(null,mongoose.Types.ObjectId(result._id))
                );
            );
         ,
        function(cb)
            body.genres.forEach(function(elem)
                Genre.findOne(id: elem,enter code here function(err, result)
                    cb(null,mongoose.Types.ObjectId(result._id));
                )
            );
         ],function(err,results)
            //here you'll get an array of results ordered by your tasks
                if(!err)
                    platforms.push(results[0])
                    genres.push(results[1])
                
             )

我没有运行此代码,但就是这样,如果您需要更多信息,可以阅读文档:http://caolan.github.io/async/docs.html

【讨论】:

【参考方案3】:

这是一个不错的 Promise 用例(它是一个出色的工具,使您能够轻松地执行异步操作),将来应该会对您有所帮助。

当前代码的问题是findOne 操作是异步的,并且会在一段时间后完成。同时,下一行将开始执行。因此,当您到达save 状态时,findOne 都不会完成,您会得到空数组

实现 Promise 的两个流行的 nodejs 库是 Q 和 Bluebird。最新版本的 NodeJS 也实现了默认的Promise

以下是使用 Bluebird 的代码。您基本上必须为涉及平台和流派中的findOne 的每个数据库操作创建承诺。当所有这些都完成后,您必须开始执行最后的save 部分。这是使用 Promise.all 功能实现的,该功能将等待所有承诺完成。

var Promise = require('bluebird')

var platformPromises = []; //load platforms
body.release_dates.forEach(function(elem)
    platformPromises.push(new Promise(function (resolve, reject) 
        Platform.findOne( id : elem.platform, function(err, result) 
            if(err) 
                reject(err);
            else
                resolve(mongoose.Types.ObjectId(result._id));
        );
    ))

);

var genrePromises = []; //load genre
body.genres.forEach(function(elem)
    genrePromises.push(new Promise(function (resolve, reject) 
        Genre.findOne(id: elem, function(err, result)
            if(err) 
                reject(err);
            else
                resolve(mongoose.Types.ObjectId(result._id));
        );
    ))

);

var allPromises = platformPromises.concat(genrePromises);

Promise.all(allPromises).then(function (result) 
    //prepare to save!

    var platforms = [];
    var genres = [];

    for(var i=0; i<platformPromises.length; i++)
        platforms.push(result[i]); // result come out in same order as the promises

    for(var i=platformPromises.length; i<result.length; i++)
        genres.push(result[i]);

    var game = 
        igdb_id : body.id,
        name : body.name,
        summary : body.summary,
        storyline : body.description,
        genres : genres,
        platforms : platforms,
        release_date : body.original_release_date,
        cover : body.cover.cloudinary_id,
        videos: body.videos
    ;

    var data = new Game(game);
    data.save(function(err, game)
        if(err)
            res.send("500");
            return console.error(err);
        

    );

)

【讨论】:

如果你不使用回调函数,Mongoose 已经返回了 Promise。 感谢您指出这一点。只是想为来自同步背景的人解释如何使用 Promise。 @hyades,很好,但是如果你有三个数组要得到,你只需在“结果”中混合 platformPromises 和genrePromises,如果有三个数组,“for”函数会是什么样子?例如,platformPromises、genrePromises、themePromises @tonywei 是的,结果数组将与输入数组的顺序相同,

以上是关于Node.JS + Mongoose 回调地狱的主要内容,如果未能解决你的问题,请参考以下文章

Node.js回调地狱及使用Promiseasync和await函数的解决方法

告别回调地狱,在Node里优雅的访问MySQL

Mongoose:我如何避免回调地狱,同时允许对不返回承诺的 mongoose 方法进行存根?

Node.js Promise对象(解决回调地狱问题)async和await函数

mongoose、express 和 node.js 中回调函数的参数

Node.js Mongoose 回调