web-dev-qa-db-ja.com

mongodbカーソルをシリアルで繰り返し処理(次のドキュメントに移動する前にコールバックを待機)

Mongoskinを使用して、カーソルを返す次のようなクエリを実行できます。

_myCollection.find({}, function(err, resultCursor) {
      resultCursor.each(function(err, result) {

      }
}
_

ただし、各ドキュメントに対していくつかの非同期関数を呼び出し、これがコールバックされた後にカーソルの次の項目に移動したいだけです(async.jsモジュールのeachSeries構造に似ています)。例えば:

_myCollection.find({}, function(err, resultCursor) {
      resultCursor.each(function(err, result) {

            externalAsyncFunction(result, function(err) {
               //externalAsyncFunction completed - now want to move to next doc
            });

      }
}  
_

どうすればこれができますか?

ありがとう

更新:

toArray()は大規模なバッチ操作であり、結果が一度にメモリに収まらない可能性があるため、使用しません。

49
UpTheCreek

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

async/awaitを使用するより現代的なアプローチ:

_const cursor = db.collection("foo").find({});
while(await cursor.hasNext()) {
  const doc = await cursor.next();
  // process doc here
}
_

注:

  • これは、さらに非同期イテレータ が到着したときに簡単に実行できます。
  • おそらく、エラーチェックのためにtry/catchを追加する必要があります。
  • 含まれる関数はasyncであるか、awaitを使用するため、コードを_(async function() { ... })()_でラップする必要があります。
  • 必要に応じて、whileループの最後にawait new Promise(resolve => setTimeout(resolve, 1000));(1秒間一時停止)を追加して、ドキュメントを次々に処理することを示します。
48
user993683

これは、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();
}
10
Daphoque

誰かが(nextObjectのコールバックを使用するのではなく)これを行うPromiseの方法を探しているなら、ここにあります。私はNode v4.2.2およびmongoドライバーv2.1.7を使用しています。これはCursor.forEach()のasyncSeriesバージョンです:

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ネイティブドライバー関数が行うように、promiseを返す必要があります。

コレクション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();
})
4
user3392439

非同期ライブラリを使用して、このようなことができます。ここで重要なのは、現在のドキュメントがnullかどうかを確認することです。もしそうなら、それはあなたが終わったことを意味します。

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);
    });
2
Antoine Desbois

Arrayで結果を取得し、次のような再帰関数を使用して反復できます。

myCollection.find({}).toArray(function (err, items) {
    var count = items.length;
    var fn = function () {
        externalAsyncFuntion(items[count], function () {
            count -= 1;
            if (count) fn();
        })
    }

    fn();
});

編集:

これは小さなデータセットにのみ適用できます。大きなデータセットの場合は、他の回答で述べたようにカーソルを使用する必要があります。

0
Salman

Futureを使用できます:

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

        }
    }));
});
0
Gerard Carbó

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
}

Jake Archibaldは、非同期イテレータについて すばらしいブログ記事 を書きました。これは、@ user993683の答えを読んだ後に知りました。

0
Jaydeep Solanki