web-dev-qa-db-fra.com

Itération sur un curseur mongodb en série (en attente de rappels avant de passer au document suivant)

En utilisant mongoskin, je peux faire une requête comme celle-ci, qui retournera un curseur:

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

      }
}

Cependant, je voudrais appeler certaines fonctions asynchrones pour chaque document et ne passer à l'élément suivant sur le curseur qu'après que cela a été rappelé (similaire à la structure eachSeries du module async.js). Par exemple:

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

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

      }
}  

Comment pourrais-je faire ça?

Merci

MISE À JOUR:

Je ne veux pas utiliser toArray() car il s'agit d'une opération par lots de grande taille, et les résultats peuvent ne pas tenir en mémoire en une seule fois.

49
UpTheCreek

Si vous ne souhaitez pas charger tous les résultats en mémoire à l'aide de toArray, vous pouvez itérer à l'aide du curseur avec quelque chose comme ce qui suit.

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
Timothy Strimple

Une approche plus moderne qui utilise async/await:

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

Remarques:

  • Cela peut être encore plus simple à faire quand itérateurs asynchrones arrivent.
  • Vous voudrez probablement ajouter try/catch pour la vérification des erreurs.
  • La fonction conteneur doit être async ou le code doit être encapsulé dans (async function() { ... })() Car il utilise await.
  • Si vous le souhaitez, ajoutez await new Promise(resolve => setTimeout(resolve, 1000)); (pause pendant 1 seconde) à la fin de la boucle while pour montrer qu'il traite les documents les uns après les autres.
48
user993683

Cela fonctionne avec un grand ensemble de données en utilisant 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

Si quelqu'un cherche un moyen Promise de le faire (par opposition à l'utilisation des rappels de nextObject), le voici. J'utilise Node v4.2.2 et mongo driver v2.1.7. C'est une sorte de version asyncSeries de 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);
  });
}

Pour l'utiliser, passez le curseur et un itérateur qui opère sur chaque document de manière asynchrone (comme vous le feriez pour Cursor.forEach). L'itérateur doit retourner une promesse, comme le font la plupart des fonctions de pilote natif mongodb.

Supposons que vous souhaitiez mettre à jour tous les documents de la collection test. Voici comment vous le feriez:

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

Vous pouvez faire quelque chose comme ça en utilisant la bibliothèque async. Le point clé ici est de vérifier si le document actuel est nul. Si c'est le cas, cela signifie que vous avez terminé.

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

Vous pouvez obtenir le résultat dans un Array et itérer en utilisant une fonction récursive, quelque chose comme ça.

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

    fn();
});

Modifier:

Cela ne s'applique qu'aux petits ensembles de données, pour les plus grands, vous devez utiliser les curseurs comme mentionné dans d'autres réponses.

0
Salman

Vous pouvez utiliser un avenir:

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ó

depuis node.js v10. vous pouvez utiliser l'itérateur asynchrone

const cursor = db.collection('foo').find({});
for await (const doc of cursor) {
  // do your thing
  // you can even use `await myAsyncOperation()` here
}

Jake Archibald a écrit n excellent article de blog sur les itérateurs asynchrones, que j'ai appris après avoir lu la réponse de @ user993683.

0
Jaydeep Solanki