¿Cómo puedo usar un cursor.forEach () en MongoDB usando Node.js?

Tengo una enorme colección de documentos en mi base de datos y me pregunto cómo puedo ejecutar todos los documentos y actualizarlos, cada documento con un valor diferente.

La respuesta depende del controlador que estás usando. Todos los controladores de MongoDB que conozco tienen cursor.forEach() implementado de una forma u otra.

Aquí hay unos ejemplos:

nodo-mongodb-nativo

 collection.find(query).forEach(function(doc) { // handle }, function(err) { // done or error }); 

mongojs

 db.collection.find(query).forEach(function(err, doc) { // handle }); 

monje

 collection.find(query, { stream: true }) .each(function(doc){ // handle doc }) .error(function(err){ // handle error }) .success(function(){ // final callback }); 

mongoose

 collection.find(query).stream() .on('data', function(doc){ // handle doc }) .on('error', function(err){ // handle error }) .on('end', function(){ // final callback }); 

Actualización de documentos dentro de .forEach callback

El único problema con la actualización de documentos dentro de la callback de .forEach es que no tiene idea de cuándo se actualizan todos los documentos.

Para resolver este problema, debe usar alguna solución de flujo de control asincrónico. Aquí hay algunas opciones:

  • asincrónico
  • promesas ( when.js , bluebird )

Aquí hay un ejemplo del uso de async , usando su función de queue :

 var q = async.queue(function (doc, callback) { // code for your update collection.update({ _id: doc._id }, { $set: {hi: 'there'} }, { w: 1 }, callback); }, Infinity); var cursor = collection.find(query); cursor.each(function(err, doc) { if (err) throw err; if (doc) q.push(doc); // dispatching doc to async.queue }); q.drain = function() { if (cursor.isClosed()) { console.log('all items have been processed'); db.close(); } } 

La respuesta de Leonid es genial, pero quiero reforzar la importancia de usar asincronías / promesas y dar una solución diferente con un ejemplo de promesas.

La solución más simple para este problema es hacer un ciclo para cada documento y llamar a una actualización. Por lo general, no necesita cerrar la conexión db después de cada solicitud , pero si necesita cerrar la conexión, tenga cuidado. Solo debe cerrarlo si está seguro de que todas las actualizaciones se han terminado de ejecutar.

Un error común aquí es llamar a db.close() después de que se envíen todas las actualizaciones sin saber si se han completado. Si haces eso, obtendrás errores.

Implementación incorrecta :

 collection.find(query).each(function(err, doc) { if (err) throw err; if (doc) { collection.update(query, update, function(err, updated) { // handle }); } else { db.close(); // if there is any pending update, it will throw an error there } }); 

Sin embargo, como db.close() también es una operación de sincronización ( su firma tiene una opción de callback) puede tener suerte y este código puede finalizar sin errores. Puede funcionar solo cuando necesite actualizar solo unos pocos documentos en una colección pequeña (por lo tanto, no intente).

Solución correcta:

Como Leonid ya propuso una solución con asincrónica, a continuación se presenta una solución que utiliza promesas Q.

 var Q = require('q'); var client = require('mongodb').MongoClient; var url = 'mongodb://localhost:27017/test'; client.connect(url, function(err, db) { if (err) throw err; var promises = []; var query = {}; // select all docs var collection = db.collection('demo'); var cursor = collection.find(query); // read all docs cursor.each(function(err, doc) { if (err) throw err; if (doc) { // create a promise to update the doc var query = doc; var update = { $set: {hi: 'there'} }; var promise = Q.npost(collection, 'update', [query, update]) .then(function(updated){ console.log('Updated: ' + updated); }); promises.push(promise); } else { // close the connection after executing all promises Q.all(promises) .then(function() { if (cursor.isClosed()) { console.log('all items have been processed'); db.close(); } }) .fail(console.error); } }); }); 
var MongoClient = require('mongodb').MongoClient, assert = require('assert'); MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) { assert.equal(err, null); console.log("Successfully connected to MongoDB."); var query = { "category_code": "biotech" }; db.collection('companies').find(query).toArray(function(err, docs) { assert.equal(err, null); assert.notEqual(docs.length, 0); docs.forEach(function(doc) { console.log(doc.name + " is a " + doc.category_code + " company."); }); db.close(); }); });
var MongoClient = require('mongodb').MongoClient, assert = require('assert'); MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) { assert.equal(err, null); console.log("Successfully connected to MongoDB."); var query = { "category_code": "biotech" }; db.collection('companies').find(query).toArray(function(err, docs) { assert.equal(err, null); assert.notEqual(docs.length, 0); docs.forEach(function(doc) { console.log(doc.name + " is a " + doc.category_code + " company."); }); db.close(); }); }); 

Observe que la llamada .toArray hace que la aplicación .toArray el conjunto de datos completo.

var MongoClient = require('mongodb').MongoClient, assert = require('assert'); MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) { assert.equal(err, null); console.log("Successfully connected to MongoDB."); var query = {"category_code": "biotech"}; var cursor = db.collection('companies').find(query); function(doc) { cursor.forEach( console.log( doc.name + " is a " + doc.category_code + " company." ); }, function(err) { assert.equal(err, null); return db.close(); } ); });
var MongoClient = require('mongodb').MongoClient, assert = require('assert'); MongoClient.connect('mongodb://localhost:27017/crunchbase', function(err, db) { assert.equal(err, null); console.log("Successfully connected to MongoDB."); var query = {"category_code": "biotech"}; var cursor = db.collection('companies').find(query); function(doc) { cursor.forEach( console.log( doc.name + " is a " + doc.category_code + " company." ); }, function(err) { assert.equal(err, null); return db.close(); } ); }); 

Observe que el cursor devuelto por find() está asignado al var cursor . Con este enfoque, en lugar de buscar todos los datos en el memort y consumir datos a la vez, estamos transmitiendo los datos a nuestra aplicación. find() puede crear un cursor inmediatamente porque en realidad no realiza una solicitud a la base de datos hasta que intentemos usar algunos de los documentos que proporcionará. El punto del cursor es describir nuestra consulta. El 2do parámetro para cursor.forEach muestra qué hacer cuando el controlador se agota o se produce un error.

En la versión inicial del código anterior, toArray() forzó la llamada a la base de datos. Significaba que necesitábamos TODOS los documentos y queríamos que estuvieran en una array .

Además, MongoDB devuelve datos en formato de lote. La imagen a continuación muestra las solicitudes de los cursores (desde la aplicación) a MongoDB

Solicitudes de cursor MongoDB

forEach es mejor que toArray porque podemos procesar documentos a medida que llegan hasta que llegamos al final. Contraste con toArray , donde esperamos que se recuperen TODOS los documentos y que se construya todo el conjunto. Esto significa que no obtendremos ninguna ventaja del hecho de que el controlador y el sistema de base de datos están trabajando juntos para generar resultados por lotes para su aplicación. El procesamiento por lotes está diseñado para proporcionar eficiencia en términos de sobrecarga de memoria y tiempo de ejecución. Aproveche esto, si puede en su aplicación .

Y aquí hay un ejemplo del uso de un cursor Mongoose asincrónico con promesas:

 new Promise(function (resolve, reject) { collection.find(query).cursor() .on('data', function(doc) { // ... }) .on('error', reject) .on('end', resolve); }) .then(function () { // ... }); 

Referencia:

  • Cursores de mongoose
  • Corrientes y promesas

El node-mongodb-native ahora admite un parámetro endCallback para cursor.forEach como para manejar el evento DESPUÉS de la iteración completa, consulte el documento oficial para obtener detalles http://mongodb.github.io/node-mongodb-native/ 2.2 / api / Cursor.html # forEach .

También tenga en cuenta que .each está en desuso en el controlador nativo del nodojs ahora.