firebase 更新事务内多个文档的正确方法

fbcarpbf  于 2023-02-05  发布在  其他
关注(0)|答案(1)|浏览(101)
    • TL; DR**我应该使用Transaction.getAll()还是使用for循环并使用Transaction.get()逐个更新文档

考虑下面的模式:

- someTopLevelCollection
   - accountId1
       - projects
       - tasks
       - users
  - accountId2
       - projects
       - tasks
       - users

每当分别在projects、tasks子集合中创建项目/任务时,用户集合中的计数器即projectsCount和tasksCount被更新。
对用户的引用作为userId数组保存在项目和任务中,如下所示:
注:为简洁起见,删除了其他字段
项目/任务结构:

{
   "name": "someName",
   "status": "someStatus",
   "users": [
       "userId1",
       "userId2",
       ....
   ],
   ...
}

现在我需要使用事务更新users数组中所有userId的计数器。
方法一:

const accountId = context.params.accountId;
const userIds = snapshot.data().users;

userIds.forEach(userId => {

    const userDocRef = db.collection(someTopLevelCollection)
        .doc(accountId)
        .collection('users')
        .doc(userId);

    let transaction = db.runTransaction(transaction => {
        return transaction.get(userDocRef)
            .then(doc => {
                const snapshotData = doc.data();
                let newCounterValue = snapshotData[counterName] + 1;
                transaction.update(userDocRef, {counterName: newCounterValue});
                return Promise.resolve(`Incremented ${counterName} to ${newCounterValue}`);
            });

    }).then(result => {
        console.log('Transaction success!', result);
        return true;

    }).catch(err => {
        console.error('Transaction failure:', err);
        return false;
    });

});

方法二:

const accountId = context.params.accountId;
const userIds = snapshot.data().users;

let userDocRefs = [];
userIds.forEach(userId => {
    const userDocRef = db.collection(someTopLevelCollection)
        .doc(accountId)
        .collection('users')
        .doc(userId);
    userDocRefs.push(userDocRef)
});

let transaction = db.runTransaction(transaction => {
    return transaction.getAll(userDocRefs)
        .then(docs => {

            docs.forEach(doc => {
                const snapshotData = doc.data();
                let newCounterValue = snapshotData[counterName] + 1;
                transaction.update(doc.ref, {counterName: newCounterValue});
            });
            return Promise.resolve('Completed transaction successfully');

        });

}).then(result => {
    console.log('Transaction success!', result);
    return true;

}).catch(err => {
    console.error('Transaction failure:', err);
    return false;

});

以下是我的问题:
1.当getAll()正在执行时,当文档发生外部更改时,是否重新获取所有文档以保持一致性。如果是,使用getAll()的用例是什么?
1.当使用for循环逐个运行事务,并且在事务中正在修改的当前文档外部发生文档更改时,是否仅为该文档重试事务
谢谢。

58wvjzkj

58wvjzkj1#

首先是简短的回答,然后是解释:
1.是的,当getAll()中的引用发生外部更改时,整个事务将被重试(即所有文档将被重新读取)。
1.**不,事务不会只为一个文档重试。**事务是全有或全无的。它要么全部成功,要么在失败时重新运行所有操作(包括所有读操作和所有写操作)。但是,根据您提供的上下文,答案更微妙;下面还有更多。

getAll是怎么回事?

你的第一个问题的深层答案是微妙的,因为Firestore locks differently between mobile/web and server

  • 由于移动的/web的延迟较长,事务会乐观地锁定文档引用。因此,如果您来自mobile/web,则在事务执行过程中,确实可能会对某个文档执行写入操作。在这种情况下,整个事务将被重试(也就是说,您传递给db.runTransaction的整个函数将再次运行-也就是说,您将从字面上重新拉取所有文档的最新版本,然后尝试写入它们中的每一个)。
  • 但是服务器端的Firestore调用(例如使用管理界面)锁是悲观的,因为它假定服务器具有低延迟和良好的连接。当出现数据争用时,您对服务器的getAll()调用会阻止所有其他尝试写入这些文档,直到您的事务完成。换句话说,在服务器端Firestore事务中,“文档更改[to]发生在外部”是不可能的。您肯定会成功地读取文档,并且在事务处理过程中不会从外部更改文档。

然后你提到了“为什么是getAll“?原因是你想通过批处理获取来保证原子性。Firestore事务要求所有的读取都在任何写入之前完成。当你需要原子地读取和编辑大量文档时,你可能会想使用getAll
所以在你的例子“方法#1”中,你正在编辑的所有文档都不能保证被原子地修改。方法#1只提供了每个文档级别的原子性--这意味着如果这些修改需要保持一致,方法#1并不是正确的方法。我猜你会想使用方法#2。给出了用户记录如何与包含所有userId的根文档相互关联的描述。
注意,FWIW,Firestore现在支持atomic increments,它完成了方法#1想要的。原子增量保证,不需要事务,一个数字将被原子地写入一个增量值(也就是说,在您阅读和写入数字之间没有其他人写入)。但是这些原子增量仍然只能在每个文档中得到保证(好吧,每个数字)级别--所以如果您的更改需要在一堆文档中涟漪,以便彼此保持一致,您仍然需要使用方法#2。

事务重试次数

假设你在移动的或网络上,所以你的事务只持有所有文档引用的乐观锁,在这种情况下,当你在事务中时,另一个线程有可能写入你的文档。
由于事务是all-or-none的,当有人写入您在事务中读取的一个文档时,* 整个事务将重新运行 *。所有的东西都被重新读取,所有的东西都被重新更新。(不要问我是否会在失败的事务重试时再次收取读/写费用...我不知道)。Firestore客户端将自动重试您的事务五次,之后它将放弃并抛出异常。
重新尝试整个事务,而不仅仅是更改的一个文档,这是必要的,因为事务锁定的整个前提是保证一组“相互依赖的更改”要么发生在所有文档上,要么不发生在任何文档上。因此,只重新尝试单个文档的读/写不是您想要的(因为您选择使用事务,可能是因为文档更改是相互关联的)。

相关问题