【发布时间】:2014-10-19 21:35:24
【问题描述】:
我的数据库中有大量文档,我想知道如何遍历所有文档并更新它们,每个文档都有不同的值。
【问题讨论】:
-
这取决于您用于连接 MongoDB 的驱动程序。
-
我使用的是mongodb驱动
-
你能给我一些关于 forEach() 内部更新的例子,并指定你在哪里关闭与数据库的连接,因为我遇到了问题
我的数据库中有大量文档,我想知道如何遍历所有文档并更新它们,每个文档都有不同的值。
【问题讨论】:
我寻找了一个性能良好的解决方案,我最终创建了一个我认为效果很好的组合:
/**
* 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);
}
}
【讨论】:
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。使用这种方法,我们不是一次获取内存中的所有数据并使用数据,而是将数据流式传输到我们的应用程序。 find() 可以立即创建一个游标,因为它实际上不会向数据库发出请求,直到我们尝试使用它将提供的一些文档。 cursor 的重点是描述我们的查询。 cursor.forEach 的第二个参数显示了当驱动程序耗尽或发生错误时该怎么做。
在上述代码的初始版本中,强制数据库调用的是toArray()。这意味着我们需要所有文档并希望它们位于array 中。
另外,MongoDB 以批处理格式返回数据。下图显示,来自游标(来自应用程序)的请求到MongoDB
forEach 比 toArray 更好,因为我们可以在文档进入时处理,直到到达最后。将其与toArray 进行对比 - 我们等待 ALL 检索文档并构建 entire 数组。这意味着我们没有从驱动程序和数据库系统协同工作以将结果批处理到您的应用程序这一事实中获得任何优势。批处理旨在提高内存开销和执行时间方面的效率。 如果可以的话,在你的应用程序中利用它。
【讨论】:
您现在可以使用(当然是在异步函数中):
for await (let doc of collection.find(query)) {
await updateDoc(doc);
}
// all done
很好地序列化所有更新。
【讨论】:
doc 可以是const,因为它的范围在循环内。
假设我们有以下 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')})
});
【讨论】:
之前的答案都没有提到批量更新。这使得它们非常慢 ? - 比使用 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);
【讨论】:
使用mongodb 驱动程序和带有async/await 的现代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 进一步改进,以便只从数据库中获取那些需要的文档字段。
【讨论】:
while(document=....) 模式
答案取决于您使用的驱动程序。我认识的所有 MongoDB 驱动程序都以一种或另一种方式实现了 cursor.forEach()。
这里有一些例子:
collection.find(query).forEach(function(doc) {
// handle
}, function(err) {
// done or error
});
db.collection.find(query).forEach(function(err, doc) {
// handle
});
collection.find(query, { stream: true })
.each(function(doc){
// handle doc
})
.error(function(err){
// handle error
})
.success(function(){
// final callback
});
collection.find(query).stream()
.on('data', function(doc){
// handle doc
})
.on('error', function(err){
// handle error
})
.on('end', function(){
// final callback
});
.forEach 回调中更新文档在.forEach 回调中更新文档的唯一问题是您不知道所有文档何时更新。
要解决这个问题,您应该使用一些异步控制流解决方案。以下是一些选项:
这里是一个使用async的例子,使用它的queue feature:
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();
}
}
【讨论】:
.forEach 吗?
.stream 方法已被弃用,现在我们应该使用 .cursor
node-mongodb-native现在支持endCallback参数到cursor.forEach,用于处理整个迭代后的事件,详细参考官方文档http://mongodb.github.io/node-mongodb-native/2.2/api/Cursor.html#forEach。
另请注意,.each 现在在 nodejs 原生驱动程序中已弃用。
【讨论】:
forEach(iteratorCallback, endCallback) endCallback(error) 在没有更多数据时调用(error 未定义)。
下面是一个使用 Mongoose 游标与 promise 异步的示例:
new Promise(function (resolve, reject) {
collection.find(query).cursor()
.on('data', function(doc) {
// ...
})
.on('error', reject)
.on('end', resolve);
})
.then(function () {
// ...
});
参考:
【讨论】:
Leonid's answer 很棒,但我想强调使用 async/promises 的重要性,并通过 Promise 示例提供不同的解决方案。
解决这个问题最简单的方法是循环 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 已经提出了一个异步解决方案,下面遵循使用Q promises 的解决方案。
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);
}
});
});
【讨论】:
doc is not defined 的行 var promises = calls.map(myUpdateFunction(doc));