Итерация над курсором mongodb поочередно (ожидание обратных вызовов перед переходом к следующему документу)
Используя mongoskin, я могу сделать такой запрос, который вернет курсор:
myCollection.find({}, function(err, resultCursor) {
resultCursor.each(function(err, result) {
}
}
Тем не менее, я хотел бы назвать некоторые функции асинхронизации для каждого документа и только перейти к следующему элементу курсора после того, как он перезвонил (аналогично структуре eachSeries в модуле async.js). Например:
myCollection.find({}, function(err, resultCursor) {
resultCursor.each(function(err, result) {
externalAsyncFunction(result, function(err) {
//externalAsyncFunction completed - now want to move to next doc
});
}
}
Как я могу это сделать?
Спасибо
UPDATE:
Я не хочу использовать toArray()
, поскольку это большая пакетная операция, и результаты могут не соответствовать памяти за один раз.
Ответы
Ответ 1
Если вы не хотите загружать все результаты в память, используя toArray, вы можете выполнять итерацию с помощью курсора с чем-то вроде следующего.
myCollection.find({}, function(err, resultCursor) {
function processItem(err, item) {
if(item === null) {
return; // All done!
}
externalAsyncFunction(item, function(err) {
resultCursor.nextObject(processItem);
});
}
resultCursor.nextObject(processItem);
}
Ответ 2
Более современный подход, использующий async
/await
:
const cursor = db.collection("foo").find({});
while(await cursor.hasNext()) {
const doc = await cursor.next();
// process doc here
}
Заметки:
- Это может быть еще проще сделать, когда появятся асинхронные итераторы.
- Возможно, вы захотите добавить try/catch для проверки ошибок.
- Содержащая функция должна быть
async
или код должен быть заключен в (async function() {... })()
так как она использует await
. - Если хотите, добавьте
await new Promise(resolve => setTimeout(resolve, 1000));
(пауза на 1 секунду) в конце цикла while, чтобы показать, что он обрабатывает документы один за другим.
Ответ 3
Это работает с большим набором данных с помощью setImmediate:
var cursor = collection.find({filter...}).cursor();
cursor.nextObject(function fn(err, item) {
if (err || !item) return;
setImmediate(fnAction, item, arg1, arg2, function() {
cursor.nextObject(fn);
});
});
function fnAction(item, arg1, arg2, callback) {
// Here you can do whatever you want to do with your item.
return callback();
}
Ответ 4
Если кто-то ищет способ Promise сделать это (в отличие от использования обратных вызовов nextObject), вот оно. Я использую Node v4.2.2 и драйвер mongo v2.1.7. Это своего рода версия asyncSeries Cursor.forEach()
:
function forEachSeries(cursor, iterator) {
return new Promise(function(resolve, reject) {
var count = 0;
function processDoc(doc) {
if (doc != null) {
count++;
return iterator(doc).then(function() {
return cursor.next().then(processDoc);
});
} else {
resolve(count);
}
}
cursor.next().then(processDoc);
});
}
Чтобы использовать это, пропустите курсор и итератор, который работает на каждом документе асинхронно (например, для Cursor.forEach). Итератору нужно возвратить обещание, как и большинство собственных функций драйвера mongodb.
Скажем, вы хотите обновить все документы в коллекции test
. Вот как вы это сделаете:
var theDb;
MongoClient.connect(dbUrl).then(function(db) {
theDb = db; // save it, we'll need to close the connection when done.
var cur = db.collection('test').find();
return forEachSeries(cur, function(doc) { // this is the iterator
return db.collection('test').updateOne(
{_id: doc._id},
{$set: {updated: true}} // or whatever else you need to change
);
// updateOne returns a promise, if not supplied a callback. Just return it.
});
})
.then(function(count) {
console.log("All Done. Processed", count, "records");
theDb.close();
})
Ответ 5
Вы можете сделать что-то подобное, используя async lib. Ключевым моментом здесь является проверка того, является ли текущий документ нулевым. Если это так, значит, вы закончили.
async.series([
function (cb) {
cursor.each(function (err, doc) {
if (err) {
cb(err);
} else if (doc === null) {
cb();
} else {
console.log(doc);
array.push(doc);
}
});
}
], function (err) {
callback(err, array);
});
Ответ 6
Вы можете получить результат в Array
и выполнить итерацию с помощью рекурсивной функции, что-то вроде этого.
myCollection.find({}).toArray(function (err, items) {
var count = items.length;
var fn = function () {
externalAsyncFuntion(items[count], function () {
count -= 1;
if (count) fn();
})
}
fn();
});
Редактировать:
Это применимо только для небольших наборов данных, для более крупных вы должны использовать курсоры, как указано в других ответах.
Ответ 7
Вы можете использовать будущее:
myCollection.find({}, function(err, resultCursor) {
resultCursor.count(Meteor.bindEnvironment(function(err,count){
for(var i=0;i<count;i++)
{
var itemFuture=new Future();
resultCursor.nextObject(function(err,item)){
itemFuture.result(item);
}
var item=itemFuture.wait();
//do what you want with the item,
//and continue with the loop if so
}
}));
});
Ответ 8
начиная с node.js v10.3 вы можете использовать асинхронный итератор
const cursor = db.collection('foo').find({});
for await (const doc of cursor) {
// do your thing
// you can even use 'await myAsyncOperation()' here
}
Джейк Арчибальд написал отличный пост в блоге об асинхронных итераторах, о котором я узнал после прочтения ответа @user993683.
Ответ 9
Вы можете использовать простой setTimeOut. Это пример в typescript, выполняющийся на nodejs (я использую promises через модуль "когда", но это можно сделать и без них):
import mongodb = require("mongodb");
var dbServer = new mongodb.Server('localhost', 27017, {auto_reconnect: true}, {});
var db = new mongodb.Db('myDb', dbServer);
var util = require('util');
var when = require('when'); //npm install when
var dbDefer = when.defer();
db.open(function() {
console.log('db opened...');
dbDefer.resolve(db);
});
dbDefer.promise.then(function(db : mongodb.Db){
db.collection('myCollection', function (error, dataCol){
if(error) {
console.error(error); return;
}
var doneReading = when.defer();
var processOneRecordAsync = function(record) : When.Promise{
var result = when.defer();
setTimeout (function() {
//simulate a variable-length operation
console.log(util.inspect(record));
result.resolve('record processed');
}, Math.random()*5);
return result.promise;
}
var runCursor = function (cursor : MongoCursor){
cursor.next(function(error : any, record : any){
if (error){
console.log('an error occurred: ' + error);
return;
}
if (record){
processOneRecordAsync(record).then(function(r){
setTimeout(function() {runCursor(cursor)}, 1);
});
}
else{
//cursor up
doneReading.resolve('done reading data.');
}
});
}
dataCol.find({}, function(error, cursor : MongoCursor){
if (!error)
{
setTimeout(function() {runCursor(cursor)}, 1);
}
});
doneReading.promise.then(function(message : string){
//message='done reading data'
console.log(message);
});
});
});