如何使用Node.js在MongoDB中使用cursor.forEach()?

时间:2014-08-26 14:07:00

标签: node.js mongodb mongoose

我的数据库中有大量文档,我想知道如何浏览所有文档并更新它们,每个文档都有不同的值。

10 个答案:

答案 0 :(得分:106)

答案取决于您使用的驱动程序。我所知道的所有MongoDB驱动程序都以cursor.forEach()的方式实现。

以下是一些例子:

node-mongodb-native

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

mongojs

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

monk

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

更新.forEach回调

内的文档

更新.forEach回调中的文档的唯一问题是您不知道何时更新所有文档。

要解决此问题,您应该使用一些异步控制流解决方案。以下是一些选项:

以下是使用async

使用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(); } } 的示例
{{1}}

答案 1 :(得分:7)

使用mongodb驱动程序和具有异步/等待功能的现代NodeJS,一个好的解决方案是使用next()

const collection = db.collection('things')
const cursor = collection.find({
  bla: 42 // find all things where bla is 42
});
let document;
while ((document = await cursor.next())) {
  await collection.findOneAndUpdate({
    _id: document._id
  }, {
    $set: {
      blu: 43
    }
  });
}

这导致一次只需要一个文档在内存中,而不是例如开始处理文件之前接受的答案,即许多文档被吸入内存。对于“大量收藏”(根据问题),这可能很重要。

如果文档很大,则可以通过使用projection进一步加以改进,以便仅从数据库中获取所需的那些文档字段。

答案 2 :(得分:6)


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

    });

});

请注意,调用.toArray正在使应用程序获取整个数据集。


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

请注意,find()返回的光标已分配给var cursor。使用这种方法,我们不是一次获取memort中的所有数据并一次使用数据,而是将数据流式传输到我们的应用程序。 find()可以立即创建游标,因为在我们尝试使用它将提供的一些文档之前,它实际上不会向数据库发出请求。 cursor的要点是描述我们的查询。 cursor.forEach的第二个参数显示了当驱动程序耗尽或发生错误时要执行的操作。

在上面代码的初始版本中,强制数据库调用的是toArray()。这意味着我们需要所有文档,并希望它们位于array

此外,MongoDB以批处理格式返回数据。下图显示了游标(从应用程序)到MongoDB

的请求

MongoDB cursor requests

forEach优于toArray,因为我们可以处理文档进入直到我们结束。将其与toArray进行对比 - 我们等待所有要检索的文档,并构建整个数组。这意味着我们没有从驱动程序和数据库系统一起工作以将结果批量处理到您的应用程序的事实中获得任何好处。批处理旨在提供内存开销和执行时间方面的效率。 如果您可以在您的应用程序中,请充分利用它。

答案 3 :(得分:4)

Leonid's answer很棒,但我想强调使用async / promises的重要性,并提供一个与promises示例不同的解决方案。

此问题的最简单解决方案是循环forEach文档并调用更新。通常,您don't need close the db connection after each request,但如果您确实需要关闭连接,请小心。如果您确定所有更新已完成执行,则必须关闭它。

这里常见的错误是在调度所有更新后调用db.close(),而不知道它们是否已完成。如果你这样做,你就会收到错误。

执行错误

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

但是,由于db.close()也是异步操作(its signature有一个回调选项),您可能会很幸运,这段代码可以完成而不会出错。它可能仅在您需要更新小集合中的几个文档时才有效(所以,请不要尝试)。


正确的解决方案:

由于Leonid已经提出了async的解决方案,下面是使用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);
    }
  });
});

答案 4 :(得分:4)

node-mongodb-native现在支持endCallback cursor.forEach参数,以便在整个迭代后处理事件,请参阅官方文档了解详情http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#forEach。< / p>

另请注意,现在在nodejs本机驱动程序中不推荐使用.each

答案 5 :(得分:4)

这是一个使用Mongoose游标async和promises的例子:

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

参考:

答案 6 :(得分:1)

让我们假设下面的MongoDB数据就位。

Database name: users
Collection name: jobs
===========================
Documents
{ "_id" : ObjectId("1"), "job" : "Security", "name" : "Jack", "age" : 35 }
{ "_id" : ObjectId("2"), "job" : "Development", "name" : "Tito" }
{ "_id" : ObjectId("3"), "job" : "Design", "name" : "Ben", "age" : 45}
{ "_id" : ObjectId("4"), "job" : "Programming", "name" : "John", "age" : 25 }
{ "_id" : ObjectId("5"), "job" : "IT", "name" : "ricko", "age" : 45 }
==========================

此代码:

var MongoClient = require('mongodb').MongoClient;
var dbURL = 'mongodb://localhost/users';

MongoClient.connect(dbURL, (err, db) => {
    if (err) {
        throw err;
    } else {
        console.log('Connection successful');
        var dataBase = db.db();
        // loop forEach
        dataBase.collection('jobs').find().forEach(function(myDoc){
        console.log('There is a job called :'+ myDoc.job +'in Database')})
});

答案 7 :(得分:1)

您现在可以使用(当然,在异步功能中):

for await (let doc of collection.find(query)) {
  await updateDoc(doc);
}

// all done

可以很好地序列化所有更新。

答案 8 :(得分:0)

以上所有答案均未提及批处理更新。这使它们非常慢-比使用bulkWrite的解决方案慢几十或几百倍。

比方说,您希望将每个文档中字段的值加倍。这是在固定内存消耗的情况下快速完成此操作的方法:

// Double the value of the 'foo' field in all documents
let bulkWrites = [];
const bulkDocumentsSize = 100;  // how many documents to write at once
let i = 0;
db.collection.find({ ... }).forEach(doc => {
  i++;

  // Update the document...
  doc.foo = doc.foo * 2;

  // Add the update to an array of bulk operations to execute later
  bulkWrites.push({
    replaceOne: {
      filter: { _id: doc._id },
      replacement: doc,
    },
  });

  // Update the documents and log progress every `bulkDocumentsSize` documents
  if (i % bulkDocumentsSize === 0) {
    db.collection.bulkWrite(bulkWrites);
    bulkWrites = [];
    print(`Updated ${i} documents`);
  }
});
// Flush the last <100 bulk writes
db.collection.bulkWrite(bulkWrites);

答案 9 :(得分:0)

我寻找了一种性能良好的解决方案,最终我发现了我认为很好的解决方案:

/**
 * This method will read the documents from the cursor in batches and invoke the callback
 * for each batch in parallel.
 * IT IS VERY RECOMMENDED TO CREATE THE CURSOR TO AN OPTION OF BATCH SIZE THAT WILL MATCH
 * THE VALUE OF batchSize. This way the performance benefits are maxed out since
 * the mongo instance will send into our process memory the same number of documents
 * that we handle in concurrent each time, so no memory space is wasted
 * and also the memory usage is limited.
 *
 * Example of usage:
 * const cursor = await collection.aggregate([
     {...}, ...],
     {
        cursor: {batchSize: BATCH_SIZE} // Limiting memory use
    });
 DbUtil.concurrentCursorBatchProcessing(cursor, BATCH_SIZE, async (doc) => ...)
 * @param cursor - A cursor to batch process on.
 * We can get this from our collection.js API by either using aggregateCursor/findCursor
 * @param batchSize - The batch size, should match the batchSize of the cursor option.
 * @param callback - Callback that should be async, will be called in parallel for each batch.
 * @return {Promise<void>}
 */
static async concurrentCursorBatchProcessing(cursor, batchSize, callback) {
    let doc;
    const docsBatch = [];

    while ((doc = await cursor.next())) {
        docsBatch.push(doc);

        if (docsBatch.length >= batchSize) {
            await PromiseUtils.concurrentPromiseAll(docsBatch, async (currDoc) => {
                return callback(currDoc);
            });

            // Emptying the batch array
            docsBatch.splice(0, docsBatch.length);
        }
    }

    // Checking if there is a last batch remaining since it was small than batchSize
    if (docsBatch.length > 0) {
        await PromiseUtils.concurrentPromiseAll(docsBatch, async (currDoc) => {
            return callback(currDoc);
        });
    }
}

读取许多大文档并进行更新的用法示例:

        const cursor = await collection.aggregate([
        {
            ...
        }
    ], {
        cursor: {batchSize: BATCH_SIZE}, // Limiting memory use 
        allowDiskUse: true
    });

    const bulkUpdates = [];

    await DbUtil.concurrentCursorBatchProcessing(cursor, BATCH_SIZE, async (doc: any) => {
        const update: any = {
            updateOne: {
                filter: {
                    ...
                },
                update: {
                   ...
                }
            }
        };            

        bulkUpdates.push(update);

        // Updating if we read too many docs to clear space in memory
        await this.bulkWriteIfNeeded(bulkUpdates, collection);
    });

    // Making sure we updated everything
    await this.bulkWriteIfNeeded(bulkUpdates, collection, true);

...

    private async bulkWriteParametersIfNeeded(
    bulkUpdates: any[], collection: any,
    forceUpdate = false, flushBatchSize) {

    if (bulkUpdates.length >= flushBatchSize || forceUpdate) {
        // concurrentPromiseChunked is a method that loops over an array in a concurrent way using lodash.chunk and Promise.map
        await PromiseUtils.concurrentPromiseChunked(bulkUpsertParameters, (upsertChunk: any) => {
            return techniquesParametersCollection.bulkWrite(upsertChunk);
        });

        // Emptying the array
        bulkUpsertParameters.splice(0, bulkUpsertParameters.length);
    }
}