rahulserver
rahulserver

Reputation: 11205

Firestore transaction based on non existent document (Collection level locking not available)

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:

  1. Executes a query on users collection to check if a document exists where username=something
  2. If it does exist, fail and return error from transaction
  3. If it does not exist, just run the write operation for the userId I want to update/create.

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

Answers (2)

Kato
Kato

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

Doug Stevenson
Doug Stevenson

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

Related Questions