2

In Firestore, I have a collection of fruits containing documents with automatically generated ids, and a name property.
I want to insert a new fruit document, with an automatically generated id, and only if no other with the same name exists.
Inspired by this answer, I try this:
Edit: For the record, as detailed in the accepted answer: this code is NOT transactionally safe: it does NOT prevent race conditions which could insert the same fruit name under heavy load

const query = firestore.collection(`/fruits`).where("name", "==", "banana").limit(1);

await firestore.runTransaction(async transaction => {
    const querySnapshot = await transaction.get(query);
    if (querySnapshot.length == 0) {
        const newRef = firestore.collection(`/fruits`).doc();
        await transaction.create(newRef, { name: "banana" });
    }
});

But I wonder: is newRef guaranteed to be un-used?
Otherwise, does the transaction automatically retries (due to the create failing) until success?
Otherwise, how can I insert my fruit?

Note: I use the node.js admin SDK, but I think the problem is the same with the javascript API.

Edit: here is how I do it finally:

const hash = computeHash("banana"); // md5 or else
const uniqueRef = firestore.doc(`/fruitsNameUnique/${hash}`);

try {
    await firestore.runTransaction(async transaction => {
        transaction.create(uniqueRef, {}); // will fail if banana already exists
        const newRef = firestore.collection(`/fruits`).doc();
        transaction.create(newRef, { name: "banana" });
    });
} catch (error) {
    console.log("fruit not inserted", error.message);
}

Louis Coulet
  • 3,663
  • 1
  • 21
  • 39

1 Answers1

1

is newRef guaranteed to be un-used?

It is virtually guaranteed to be unique. The chances of two randomly generated document IDs is astronomically small.

See also:

One thing you should be aware of is that your code is not actually transactionally safe. There is nothing stopping two clients from adding a new fruit where name=banana in a race condition between the moment of the query and the moment the transaction actually creates the new document. Under low traffic situations it's probably OK, but you are taking a chance on that.

In fact, Firestore doesn't have a built-in way to ensure uniqueness of a document's field value. It will require a fair amount of extra work to implement that yourself, perhaps by using that field value as the unique key in another collection, and making sure that collection is part of a bigger transaction that deals with the documents in your fruits collection.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Thank you for the detailed answer. I'll rely on the small collision chance for the generated id. But concerning the uniqueness, I am surprised: isn't the very point of reading my query within the transaction supposed to lock the database in order to prevent the race condition that you describe? – Louis Coulet Sep 22 '20 at 20:36
  • Firestore does not offer any lock of the full database. That doesn't scale at all, and would severely impact performance. The code you provided would only lock the documents that come from the provided query. You can transact with up to 500 individual documents at a time. https://firebase.google.com/docs/firestore/quotas#writes_and_transactions – Doug Stevenson Sep 22 '20 at 20:40
  • Oh I got totally confused about what the transaction was locking but you light my candle, thank you! Thankfully I only have a single field that I want to keep unique, so I will use the hash of its value as my doc id. – Louis Coulet Sep 22 '20 at 20:56