基于不存在文档的 Firestore 事务(集合级别锁定不可用)
Firestore transaction based on non existent document (Collection level locking not available)
基于 SO 回答我了解到 firestore 在事务中没有集合级别锁定。
就我而言,在写入集合之前,我必须确保用户集合中的用户名字段是唯一的。
为此,我编写了一个执行此操作的事务:
- 对用户集合执行查询以检查是否存在用户名=某物的文档
- 如果确实存在,则交易失败并 return 错误
- 如果不存在,就运行对我要update/create的userId进行写操作update/create。
现在的问题是,如果两个客户端同时尝试 运行 此事务,则两者都可能查询集合,并且由于集合未锁定,一个客户端可能 insert/update 集合中的文档而其他人则看不到。
我的假设正确吗?如果是,那么如何处理这种情况?
您尝试执行的操作实际上不可能以原子方式执行,因为无法在无法使用 ID 识别的文档上安全地进行交易。这里的问题是,只有当您可以 get()
添加或修改特定文档时,交易才是“安全的”。由于您不能 get()
文档使用文档中的字段值,所以您不知所措。
如果您想确保 Firestore 中任何内容的唯一性,则需要将该唯一性编码到文档 ID 本身中。在最简单的情况下,您可以使用用户名作为新集合中文档的 ID。如果这样做,您的交易可以简单地 get()
用户名所需的文件,检查它是否存在,如果不存在则写入该文件。否则,交易可能会失败。
请记住,由于在 Firestore 中有 limitations 记录 ID,如果您的用户名可能违反规则,您可能需要对该用户名进行转义或编码。
将此数据编码到文档 ID 的另一种方法是使用单独的集合作为一种手动索引。然后安全规则可以强制索引的唯一性。所以像这样:
/docs/${documentId} => {uniqueField: "foo", ...}
/docmap/${uniqueField} => {docId: "doc2"}
这里的想法是,在允许他们写入文档之前,必须先写入包含新文档 ID 的文档映射条目。由于 docmap 以我们的唯一字段为关键字,因此它强制执行唯一性。
安全规则大致如下所示:
function getPath(childPath) {
return path('/databases/'+database+'/documents/'+childPath)
}
// we can only write to our doc if the unique field exists in docmap/
// and matches our doc id
match /docs/{docid} {
let docMapPath = 'docmap/' + request.resource.data.uniqueField;
allow write: if getData(docMapPath).docId == docId;
//todo validate data schema
}
// It is only possible to add a uniqueField to the docmap
// if it doesn't already exist for another doc
// we also validate that the doc id matches our schema
match /docmap/{uniqueField} {
allow write: if resource.data.size() == 0 &&
request.resource.data.docId is string &&
request.resource.data.docId.size() < 100
}
写入大致如下所示:
const db = firebase.firestore();
db.doc('docmap/foo').set('doc2')
.then(() => db.doc('docs/doc2').set({uniqueField: 'foo'})
.then(doc => console.log("success"))
.catch(e => console.error(e));
您也可以在事务甚至批处理操作中执行此操作以使其成为原子操作,但可能没有必要增加流程的复杂性;安全规则将强制执行约束。
基于
- 对用户集合执行查询以检查是否存在用户名=某物的文档
- 如果确实存在,则交易失败并 return 错误
- 如果不存在,就运行对我要update/create的userId进行写操作update/create。
现在的问题是,如果两个客户端同时尝试 运行 此事务,则两者都可能查询集合,并且由于集合未锁定,一个客户端可能 insert/update 集合中的文档而其他人则看不到。
我的假设正确吗?如果是,那么如何处理这种情况?
您尝试执行的操作实际上不可能以原子方式执行,因为无法在无法使用 ID 识别的文档上安全地进行交易。这里的问题是,只有当您可以 get()
添加或修改特定文档时,交易才是“安全的”。由于您不能 get()
文档使用文档中的字段值,所以您不知所措。
如果您想确保 Firestore 中任何内容的唯一性,则需要将该唯一性编码到文档 ID 本身中。在最简单的情况下,您可以使用用户名作为新集合中文档的 ID。如果这样做,您的交易可以简单地 get()
用户名所需的文件,检查它是否存在,如果不存在则写入该文件。否则,交易可能会失败。
请记住,由于在 Firestore 中有 limitations 记录 ID,如果您的用户名可能违反规则,您可能需要对该用户名进行转义或编码。
将此数据编码到文档 ID 的另一种方法是使用单独的集合作为一种手动索引。然后安全规则可以强制索引的唯一性。所以像这样:
/docs/${documentId} => {uniqueField: "foo", ...}
/docmap/${uniqueField} => {docId: "doc2"}
这里的想法是,在允许他们写入文档之前,必须先写入包含新文档 ID 的文档映射条目。由于 docmap 以我们的唯一字段为关键字,因此它强制执行唯一性。
安全规则大致如下所示:
function getPath(childPath) {
return path('/databases/'+database+'/documents/'+childPath)
}
// we can only write to our doc if the unique field exists in docmap/
// and matches our doc id
match /docs/{docid} {
let docMapPath = 'docmap/' + request.resource.data.uniqueField;
allow write: if getData(docMapPath).docId == docId;
//todo validate data schema
}
// It is only possible to add a uniqueField to the docmap
// if it doesn't already exist for another doc
// we also validate that the doc id matches our schema
match /docmap/{uniqueField} {
allow write: if resource.data.size() == 0 &&
request.resource.data.docId is string &&
request.resource.data.docId.size() < 100
}
写入大致如下所示:
const db = firebase.firestore();
db.doc('docmap/foo').set('doc2')
.then(() => db.doc('docs/doc2').set({uniqueField: 'foo'})
.then(doc => console.log("success"))
.catch(e => console.error(e));
您也可以在事务甚至批处理操作中执行此操作以使其成为原子操作,但可能没有必要增加流程的复杂性;安全规则将强制执行约束。