如何使用 Batch 在 Firestore 中更新 500 多个文档?

IT技术 javascript firebase google-cloud-firestore firebase-admin
2021-03-10 07:34:13

我正在尝试使用超过 500 个文档的集合中timestampFirestore管理员时间戳更新字段

const batch = db.batch();
const serverTimestamp = admin.firestore.FieldValue.serverTimestamp();

db
  .collection('My Collection')
  .get()
  .then((docs) => {
    serverTimestamp,
  }, {
    merge: true,
  })
  .then(() => res.send('All docs updated'))
  .catch(console.error);

这会引发错误

{ Error: 3 INVALID_ARGUMENT: cannot write more than 500 entities in a single call
    at Object.exports.createStatusError (C:\Users\Growthfile\Desktop\cf-test\functions\node_modules\grpc\src\common.js:87:15)
    at Object.onReceiveStatus (C:\Users\Growthfile\Desktop\cf-test\functions\node_modules\grpc\src\client_interceptors.js:1188:28)
    at InterceptingListener._callNext (C:\Users\Growthfile\Desktop\cf-test\functions\node_modules\grpc\src\client_interceptors.js:564:42)
    at InterceptingListener.onReceiveStatus (C:\Users\Growthfile\Desktop\cf-test\functions\node_modules\grpc\src\client_interceptors.js:614:8)
    at callback (C:\Users\Growthfile\Desktop\cf-test\functions\node_modules\grpc\src\client_interceptors.js:841:24)
  code: 3,
  metadata: Metadata { _internal_repr: {} },
  details: 'cannot write more than 500 entities in a single call' }

有没有一种方法可以编写一个递归方法,该方法创建一个批处理对象,逐个更新一批 500 个文档,直到更新所有文档。

从文档中我知道可以使用此处提到的递归方法进行删除操作:

https://firebase.google.com/docs/firestore/manage-data/delete-data#collections

但是,对于更新,我不确定如何结束执行,因为文档没有被删除。

6个回答

我还遇到了更新 Firestore 集合中 500 多个文档的问题。我想分享我是如何解决这个问题的。

我使用云函数来更新我在 Firestore 中的集合,但这也应该适用于客户端代码。

该解决方案计算对批处理进行的每个操作,并在达到限制后创建一个新批处理并将其推送到batchArray.

在所有更新完成后,代码循环遍历batchArray并提交数组内的每个批次。

计算 set(), update(), delete()对批次进行的每个操作重要,因为它们都计入 500 次操作限制。

const documentSnapshotArray = await firestore.collection('my-collection').get();

const batchArray = [];
batchArray.push(firestore.batch());
let operationCounter = 0;
let batchIndex = 0;

documentSnapshotArray.forEach(documentSnapshot => {
    const documentData = documentSnapshot.data();

    // update document data here...

    batchArray[batchIndex].update(documentSnapshot.ref, documentData);
    operationCounter++;

    if (operationCounter === 499) {
      batchArray.push(firestore.batch());
      batchIndex++;
      operationCounter = 0;
    }
});

batchArray.forEach(async batch => await batch.commit());

return;
@Mihae Kheel 是的,循环在达到 500 次操作后会创建一个新批次,但对每个操作进行计数很重要。您还需要某种形式的错误处理。
2021-04-27 07:34:13
您如何确保所有批次都成功执行,因为只有批次中的操作是原子的。有的批次执行有的没有,会导致数据不一致
2021-04-28 07:34:13
@Adarsh 是的,你是对的。我省略了错误处理部分。我很快会将此部分添加到答案中。我已将我的数据库更新为一个新的数据模型,这在我的案例中是幂等操作。所以我可以重复代码,直到每批都成功。
2021-05-03 07:34:13
因此,您可以做几件事。创建云函数时可以勾选重试选项。这将确保您的云函数在任何异常时执行。但是你必须处理你认为的失败,transient否则它会变成一个无限循环。此外,必须在云函数执行之间维护某种状态,以便之前执行的批次不会再次执行。也许您可以在每次成功的批处理操作时写入实时数据库/firestore,并在下一次重试中没有某个批处理时从那里继续
2021-05-03 07:34:13
或者,您可以编写作业详细信息(更新详细信息)来/queue/pendingUpdates/编写一个按计划(例如每 5 分钟)运行的云函数,该函数执行更新。操作成功后,您可以将作业删除/标记为已完成。否则它会在下一个时间间隔自动重试。这比第一个容易得多。你的意见?
2021-05-03 07:34:13

我喜欢这个简单的解决方案:

const users = await db.collection('users').get()

const batches = _.chunk(users.docs, 500).map(userDocs => {
    const batch = db.batch()
    userDocs.forEach(doc => {
        batch.set(doc.ref, { field: 'myNewValue' }, { merge: true })
    })
    return batch.commit()
})

await Promise.all(batches)

只记得import * as _ from "lodash"在顶部添加基于这个答案

“使用typescript”......我没有看到任何typescript
2021-04-28 07:34:13
这应该是官方文档的一部分。或者至少是类似的东西,不依赖于 lodash。奇迹般有效!:)
2021-05-09 07:34:13
@MattFletcher loadash 在 Vanilla JS 中编写,如果你想要类型支持安装 @types/lodash
2021-05-11 07:34:13

如上所述,@Sebastian 的回答很好,我也赞成。尽管在一次性更新 25000 多个文档时遇到了问题。逻辑调整如下。

console.log(`Updating documents...`);
let collectionRef = db.collection('cities');
try {
  let batch = db.batch();
  const documentSnapshotArray = await collectionRef.get();
  const records = documentSnapshotArray.docs;
  const index = documentSnapshotArray.size;
  console.log(`TOTAL SIZE=====${index}`);
  for (let i=0; i < index; i++) {
    const docRef = records[i].ref;
    // YOUR UPDATES
    batch.update(docRef, {isDeleted: false});
    if ((i + 1) % 499 === 0) {
      await batch.commit();
      batch = db.batch();
    }
  }
  // For committing final batch
  if (!(index % 499) == 0) {
    await batch.commit();
  }
  console.log('write completed');
} catch (error) {
  console.error(`updateWorkers() errored out : ${error.stack}`);
  reject(error);
}

您可以使用默认的BulkWriter此方法使用 500/50/5 规则。

例子:

let bulkWriter = firestore.bulkWriter();

bulkWriter.create(documentRef, {foo: 'bar'});
bulkWriter.update(documentRef2, {foo: 'bar'});
bulkWriter.delete(documentRef3);
await close().then(() => {
  console.log('Executed all writes');
});

简单的解决方案 只需开火两次?我的数组是“resultsFinal”我触发一次批处理,限制为 490,第二次触发数组长度的限制 (results.lenght) 对我来说很好用 :) 你如何检查它?你去 firebase 并删除你的收藏,firebase 说你已经删除了 XXX 个文档,与你的数组的长度相同?好的,你可以走了

async function quickstart(results) {
    // we get results in parameter for get the data inside quickstart function
    const resultsFinal = results;
    // console.log(resultsFinal.length);
    let batch = firestore.batch();
    // limit of firebase is 500 requests per transaction/batch/send 
    for (i = 0; i < 490; i++) {
        const doc = firestore.collection('testMore490').doc();
        const object = resultsFinal[i];
        batch.set(doc, object);
    }
    await batch.commit();
    // const batchTwo = firestore.batch();
    batch = firestore.batch();

    for (i = 491; i < 776; i++) {
        const objectPartTwo = resultsFinal[i];
        const doc = firestore.collection('testMore490').doc();
        batch.set(doc, objectPartTwo);
    }
    await batch.commit();

}