Reputation: 11205
Based on this SO answer I came to know that firestore does not have collection level locking in a transaction. In my case, I have to ensure that the username field in users collection is unique before I write to a collection. For that, I write a transaction that does this:
Now the issue here is that if two clients simultaneously try to run this transaction, both might query the collection and since the collection is not locked, one client might insert/update a document in collection while other won't see it.
Is my assumption correct? And if yes, then how to deal with such scenarios?
Upvotes: 4
Views: 1103
Reputation: 40582
An alternative to coding this data into the doc id is to use a separate collection as a sort of manual index. Security rules can then enforce uniqueness on the index. So something like this:
/docs/${documentId} => {uniqueField: "foo", ...}
/docmap/${uniqueField} => {docId: "doc2"}
The idea here is that one must first write the docmap entry containing the new doc id before they are allowed to writet he doc. Since the docmap is keyed on our unique field, it enforces uniqueness.
Security rules would look roughly like so:
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
}
And a write would look roughly like so:
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));
You could also do this in a transaction or even a batch operation to make it atomic, but it's probably not necessary to add complexity to the process; the security rules will enforce the constraints.
Upvotes: 1
Reputation: 317372
What you're trying to do is actually not possible to do atomically, as it's not possible to transact safely on a document that you can't identify with an ID. The problem here is that a transaction is only "safe" if you can get()
the specific document to add or modify. Since you can't get()
a document using a field value in the document, you're at a loss.
If you want to ensure uniqueness of anything in Firestore, that uniqueness will need to be coded into the document ID itself. In the simplest case, you can use the username as the ID of a document in a new collection. If you do that, your transaction can simply get()
the required document by username, check to see if it exists, then write the document if it doesn't. Else, the transaction can fail.
Bear in mind that because there are limitations to document IDs in Firestore, you might need to escape or encode that username if your usernames could possibly violate the rules.
Upvotes: 5