延迟从mongodb nodejs返回集合数组

时间:2018-03-23 17:46:25

标签: javascript node.js mongodb asynchronous promise

我需要使用express和mongodb模块检索集合列表。 首先,我检索了一个有效的集合名称列表,然后我在循环中检索那些给定集合的数据。我的问题出在getColAsync():

getColAsync()   {
    return new Promise((resolve, reject)    =>    {
        this.connectDB().then((db)  =>  {
            var allCols = [];
            let dbase = db.db(this.databaseName);
            dbase.listCollections().toArray((err, collectionNames)  =>  {
                if(err) {
                    console.log(err);
                    reject(err);
                }
                else    {
                    for(let i = 0; i < collectionNames.length; i++) {
                        dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) =>  {
                            console.log("current collection data: " + collectionData);
                            allCols[i] = collectionData;
                        })
                    }
                    console.log("done getting all data");
                    resolve(allCols);
                }
            })
        })
    })
}

connectDB() {
    if(this.dbConnection)   {
        // if connection exists
        return this.dbConnection;
    }
    else    {
        this.dbConnection = new Promise((resolve, reject) =>    {
            mongoClient.connect(this.URL, (err, db) =>  {
                if(err) {
                    console.log("DB Access: Error on mongoClient.connect.");
                    console.log(err);
                    reject(err);
                }
                else    {
                    console.log("DB Access: resolved.");
                    resolve(db);
                }
            });
        });
        console.log("DB Access: db exists. Connected.");
        return this.dbConnection;
    }
}

在我检索每个集合的forloop中,调用console.log(“完成获取所有数据”)并在forloop开始之前解析promise。例如:

done getting all data
current collection data: something
current collection data: something2
current collection data: something3

请帮忙

1 个答案:

答案 0 :(得分:1)

问题

代码中的问题是这一部分:

for (let i = 0; i < collectionNames.length; i++) {
    dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
        console.log("current collection data: " + collectionData);
        allCols[i] = collectionData;
    })
}
console.log("done getting all data");
resolve(allCols);

您应该注意到resolve(allCols);循环结束后立即调用for,但循环的每次迭代都不会等待toArray回调称为

dbase.collection(collectionNames[i].name.toString()).find({}).toArray(callback)是异步的,因此循环将结束,您将调用resolve(allCols);,但.find({}).toArray代码尚未完成。

解决方案概念

所以,基本上你做的是:

  1. 初始化结果数组allCols = []
  2. 启动一系列异步操作
  3. 返回(仍为空)结果数组
  4. 当异步操作完成时,填写现在无用的结果数组。
  5. 你应该做的是:

    1. 启动一系列异步操作
    2. 等待所有完成
    3. 从每个
    4. 获取结果
    5. 返回结果列表
    6. 关键是Promise.all([/* array of promises */])函数接受一个promises数组并返回一个Promise本身,向下游传递一个包含所有结果的数组,所以我们需要获得的是这样的:

      const dataPromises = []
      
      for (let i = 0; i < collectionNames.length; i++) {
          dataPromises[i] = /* data fetch promise */;
      }
      
      return Promise.all(dataPromises);
      

      正如您所看到的,最后一行是return Promise.all(dataPromises);而不是代码中的resolve(allCols),因此我们无法再在new Promise(func)构造函数中执行此代码。

      相反,我们应该将Promise链接到.then(),如下所示:

      getColAsync() {
          return this.connectDB().then((db) => {
              let dbase = db.db(this.databaseName);
              const dataPromises = []
              dbase.listCollections().toArray((err, collectionNames) => {
                  if (err) {
                      console.log(err);
                      return Promise.reject(err);
                  } else {
                      for (let i = 0; i < collectionNames.length; i++) {
                          dataPromises[i] = new Promise((res, rej) => {
                              dbase.collection(collectionNames[i].name.toString()).find({}).toArray((err, collectionData) => {
                                  console.log("current collection data: " + collectionData);
                                  if (err) {
                                      console.log(err);
                                      reject(err);
                                  } else {
                                      resolve(collectionData);
                                  }
                              });
                          });
                      }
                      console.log("done getting all data");
                      return Promise.all(dataPromises);
                  }
              });
          })
      }
      

      现在请注意我们返回一个return this.connectDB().then(...),然后返回Promise.all(dataPromises);这个返回的新Promise在每个步骤让我们保持Promise链,从而{ {1}}本身会返回getColAsync(),然后您可以使用Promise.then()进行处理。

      清洁代码

      你可以稍微清理你的代码:

      .catch()

      如您所见,主要变化是:

      1. 以承诺形式使用mondodb函数
      2. 使用getColAsync() { return this.connectDB().then((db) => { let dbase = db.db(this.databaseName); const dataPromises = [] // dbase.listCollections().toArray() returns a promise itself return dbase.listCollections().toArray() .then((collectionsInfo) => { // collectionsInfo.map converts an array of collection info into an array of selected // collections return collectionsInfo.map((info) => { return dbase.collection(info.name); }); }) }).then((collections) => { // collections.map converts an array of selected collections into an array of Promises // to get each collection data. return Promise.all(collections.map((collection) => { return collection.find({}).toArray(); })) }) } 轻松将数据数组转换为新数组
      3. 下面我还介绍了使用回调函数和我正在处理的模块的代码变体。

        无极混

        我最近致力于this npm module,以帮助获得更清晰,更易读的Promises组合。

        在您的情况下,我使用Array.map函数来处理您选择数据库并获取集合信息列表的第一步:

        fCombine

        这会产生一个承诺,向下游传递一个对象Promise.fCombine({ dbase: (dbURL, done) => mongoClient.connect(dbURL, done), collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), }, { dbURL: this.URL }) 。其中{dbase: /* the db instance */, collInfos: [/* the list of collections info */]}是具有回调模式的函数,如下所示:

        getCollectionNames(dbase, done)

        现在,您可以链接先前的Promise并将集合信息列表转换为选定的db集合,如下所示:

        getCollectionsInfo = (db, done) => {
            let dbase = db.db(this.databaseName);
            dbase.listCollections().toArray(done);
        }
        

        现在下游我们有一个来自db的选定集合的列表,我们应该从每个集合中获取数据,然后将数据中的结果与集合数据合并。 在我的模块中,我有一个Promise.fCombine({ dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done), collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), }, { dbURL: this.URL }).then(({ dbase, collInfos }) => { return Promise.resolve(collInfos.map((info) => { return dbase.collection(info.name); })); }) 选项,可以创建一个_mux来模仿常规PromiseMux的行为和组合模式,但它实际上在同一个Promise上工作时间。每个Promise都从下游集合数组中输入一个项目,因此您可以编写代码以从泛型集合中获取数据,并且将对数组中的每个集合执行:

        Promise

        在上面的代码中,Promise.fCombine({ dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done), collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), }, { dbURL: this.URL }).then(({ dbase, collInfos }) => { return Promise.resolve(collInfos.map((info) => { return dbase.collection(info.name); })); })._mux((mux) => { return mux._fReduce([ (collection, done) => collection.find({}).toArray(done) ]).deMux((allCollectionsData) => { return Promise.resolve(allCollectionsData); }) }); 的行为类似于_fReduce,但是它接受带回调而不是对象的函数数组,它只向下游传递最后一个函数的结果(不是结构化对象)所有的结果)。最后,_fCombine会在多路复用器的每个deMux同时执行Promise.all,并将其结果汇总在一起。

        因此整个代码看起来像这样:

        Promise

        在我的模块中,我试图避免最常见的反模式思想,但我仍然会有一些Ghost承诺。

        使用来自mongodb的承诺,这将变得更加清洁:

        getCollectionsInfo = (db, done) => {
            let dbase = db.db(this.databaseName);
            dbase.listCollections().toArray(done);
        }
        
        getCollAsync = () => {
            return Promise.fCombine({
                /**
                 * fCombine uses an object whose fields are functions with callback pattern to
                 * build consecutive Promises. Each subsequent functions gets as input the results
                 * from previous functions.
                 * The second parameter of the fCombine is the initial value, which in our case is
                 * the db url.
                 */
                dbase: ({ dbURL }, done) => mongoClient.connect(dbURL, done), // connect to DB, return the connected dbase
                collInfos: ({ dbase }, done) => getCollectionsInfo(dbase, done), // fetch collection info from dbase, return the info objects
            }, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
                return Promise.resolve(collInfos.map((info) => {
                    /**
                     *  we use Array.map to convert collection info into 
                     *  a list of selected db collections
                     */
                    return dbase.collection(info.name);
                }));
            })._mux((mux) => {
                /**
                 * _mux splits the list of collections returned before into a series of "simultaneous promises"
                 * which you can manipulate as if they were a single Promise.
                 */
                return mux._fReduce([ // this fReduce here gets as input a single collection from the retrieved list
                    (collection, done) => collection.find({}).toArray(done)
                ]).deMux((allCollectionsData) => {
                    // finally we can put back together all the results.
                    return Promise.resolve(allCollectionsData);
                })
            });
        }