- TL; DR**我应该使用
Transaction.getAll()
还是使用for循环并使用Transaction.get()
逐个更新文档
- TL; DR**我应该使用
考虑下面的模式:
- 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循环逐个运行事务,并且在事务中正在修改的当前文档外部发生文档更改时,是否仅为该文档重试事务?
谢谢。
1条答案
按热度按时间58wvjzkj1#
首先是简短的回答,然后是解释:
1.是的,当
getAll()
中的引用发生外部更改时,整个事务将被重试(即所有文档将被重新读取)。1.**不,事务不会只为一个文档重试。**事务是全有或全无的。它要么全部成功,要么在失败时重新运行所有操作(包括所有读操作和所有写操作)。但是,根据您提供的上下文,答案更微妙;下面还有更多。
getAll
是怎么回事?你的第一个问题的深层答案是微妙的,因为Firestore locks differently between mobile/web and server。
db.runTransaction
的整个函数将再次运行-也就是说,您将从字面上重新拉取所有文档的最新版本,然后尝试写入它们中的每一个)。getAll()
调用会阻止所有其他尝试写入这些文档,直到您的事务完成。换句话说,在服务器端Firestore事务中,“文档更改[to]发生在外部”是不可能的。您肯定会成功地读取文档,并且在事务处理过程中不会从外部更改文档。然后你提到了“为什么是
getAll
“?原因是你想通过批处理获取来保证原子性。Firestore事务要求所有的读取都在任何写入之前完成。当你需要原子地读取和编辑大量文档时,你可能会想使用getAll
。所以在你的例子“方法#1”中,你正在编辑的所有文档都不能保证被原子地修改。方法#1只提供了每个文档级别的原子性--这意味着如果这些修改需要保持一致,方法#1并不是正确的方法。我猜你会想使用方法#2。给出了用户记录如何与包含所有userId的根文档相互关联的描述。
注意,FWIW,Firestore现在支持atomic increments,它完成了方法#1想要的。原子增量保证,不需要事务,一个数字将被原子地写入一个增量值(也就是说,在您阅读和写入数字之间没有其他人写入)。但是这些原子增量仍然只能在每个文档中得到保证(好吧,每个数字)级别--所以如果您的更改需要在一堆文档中涟漪,以便彼此保持一致,您仍然需要使用方法#2。
事务重试次数
假设你在移动的或网络上,所以你的事务只持有所有文档引用的乐观锁,在这种情况下,当你在事务中时,另一个线程有可能写入你的文档。
由于事务是all-or-none的,当有人写入您在事务中读取的一个文档时,* 整个事务将重新运行 *。所有的东西都被重新读取,所有的东西都被重新更新。(不要问我是否会在失败的事务重试时再次收取读/写费用...我不知道)。Firestore客户端将自动重试您的事务五次,之后它将放弃并抛出异常。
重新尝试整个事务,而不仅仅是更改的一个文档,这是必要的,因为事务锁定的整个前提是保证一组“相互依赖的更改”要么发生在所有文档上,要么不发生在任何文档上。因此,只重新尝试单个文档的读/写不是您想要的(因为您选择使用事务,可能是因为文档更改是相互关联的)。