【问题标题】:delayed returning of array of collections from mongodb nodejs从 mongodb nodejs 延迟返回集合数组
【发布时间】:2018-03-23 17:46:25
【问题描述】:

我需要使用 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("done getting all data") 被调用,promise 在 forloop 开始之前就被解决了。例如:

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

请帮忙

【问题讨论】:

  • 您的主要问题是for 循环中的许多异步操作,循环不会等待,因此您在完成任何操作之前调用resolve()。您可以使用Promise.all() 等待所有这些都完成。这里有数百个解决这个普遍问题的答案。

标签: javascript node.js mongodb asynchronous promise


【解决方案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 回调被调用。 p>

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

解决方案概念

所以,基本上你所做的是:

  1. 初始化结果数组allCols = []
  2. 启动一系列异步操作
  3. 返回(仍然为空的)结果数组
  4. 随着异步操作完成,填充现在无用的结果数组。

你应该做的是:

  1. 启动一系列异步操作
  2. 等待所有完成
  3. 从每一个中获取结果
  4. 返回结果列表

其中的关键是Promise.all([/* array of promises */]) 函数,它接受一个promise 数组并返回一个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) 构造函数中执行此代码。

相反,我们应该像这样链接Promises 和.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);,这在每一步都返回一个新的Promises,让我们保持Promise链的活动,因此getColAsync()本身将返回一个@987654344 @ 然后你可以处理 .then().catch()

清洁码

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

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

如您所见,主要变化是:

  1. 以承诺形式使用 mondodb 函数
  2. 使用Array.map 轻松将数据数组转换为新数组

下面我还展示了你的代码变体,它使用带有回调的函数和我正在开发的模块。

承诺混合

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

在您的情况下,我将使用 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) 是一个具有如下回调模式的函数:

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

现在您可以链接之前的 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 选项,它创建了一个 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 执行Promise.all,将它们的结果放在一起。

因此整个代码如下所示:

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

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

使用 mongodb 的 Promise 会变得更加简洁:

getCollAsync = () => {
    return Promise.combine({
        dbase: ({ dbURL }) => { return mongoClient.connect(dbURL); },
        collInfos: ({ dbase }) => {
            return dbase.db(this.databaseName)
                .listCollections().toArray();
        },
    }, { dbURL: this.URL }).then(({ dbase, collInfos }) => {
        return Promise.resolve(collInfos.map((info) => {
            return dbase.collection(info.name);
        }));
    }).then((collections) => {
        return Promise.all(collections.map((collection) => {
            return collection.find({}).toArray();
        }))
    });
}

【讨论】:

    猜你喜欢
    • 2014-06-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-12-22
    相关资源
    最近更新 更多