19

I'm trying to run a transaction with a variable number of read operations. I put the read () operations before than update ().

Reading the Firestore doc on https://cloud.google.com/firestore/docs/manage-data/transactions

"A transaction consists of any number of get() operations followed by any number of write operations such as set(), update(), or delete()"

And

When using transactions, note that:

  • Read operations must come before write operations.
  • A function calling a transaction (transaction function) might run more than once if a current edit affects a document that the transaction reads.
  • Transaction functions should not directly modify application state.

But is not provided an implementation. When I try to run the code below, I get that the transaction function is runned more time and then I obtain an exception. But if I try with only one get all goes OK.

const reservationCol = this.db.firestore.collection('reservations');
        return this.db.firestore.runTransaction(t => {
         return Promise.all([
            t.get(reservationCol.doc('id1')),
            t.get(reservationCol.doc(('id2')))]
        ).then((responses) => {

        let found = false;
        responses.forEach(resp => {
               if (resp.exists)
                    found = true;
         });
         if (!found)
         {
               entity.id='id1';
               t.set(reservationCol.doc(entity.id), entity);
               return Promise.resolve('ok');
          }
          else
              return Promise.reject('exist');
         });
    });
Paul Roub
  • 36,322
  • 27
  • 84
  • 93
Ciro Sasso
  • 191
  • 1
  • 6
  • 3
    Did you figure this out? I have the same problem. In my case I have an array of an unknown number of firestore references, and I need to get each one, then add +1 to each and update them all. They definitely need an example of multiple gets in the docs. – Jus10 Dec 23 '17 at 03:40
  • no, I haven't updates about it, i solved using another structure that has all information inside. in this way i can do a single read. – Ciro Sasso Feb 06 '18 at 11:51

3 Answers3

11

The Firestore doc doesn't say this, but the answer is hidden in the API reference: https://cloud.google.com/nodejs/docs/reference/firestore/0.13.x/Transaction?authuser=0#getAll

You can use Transaction.getAll() instead of Transaction.get() to get multiple documents. Your example will be:

const reservationCol = this.db.firestore.collection('reservations');
return this.db.firestore.runTransaction(t => {
  return t.getAll(reservationCol.doc('id1'), reservationCol.doc('id2'))
    .then(docs => {
      const id1 = docs[0];
      const id2 = docs[1];
      if (!(id1.exists && id2.exists)) {
        // do stuff
      } else {
        // throw error
      }
    })
}).then(() => console.log('Transaction succeeded'));
sinujohn
  • 2,506
  • 3
  • 21
  • 26
  • 5
    Calling this from Angular I get `Property 'getAll' does not exist on type 'Transaction'.` It appears that `getAll` is only available in the nodejs Firestore API, not in the libraries for other environments such as Angular. – Derrick Miller Aug 12 '18 at 21:16
1

I couldn't figure out how to do this in pure Typescript, but I was able to find a JavaScript example that uses promises, so I adapted that to fit my needs. It seems to be working correctly, however when I run my function rapidly (by clicking on a button in rapid succession) I get console errors that read POST https://firestore.googleapis.com/v1beta1/projects/myprojectname/databases/(default)/documents:commit 400 (). I am unclear on whether those are errors I should be worried about, or if they're simply a a result of the transaction retrying. I posted my own question about that, and am hopeful to get some answers on it. In the meantime, here is the code that I came up with:

async vote(username, recipeId, direction) {

  let value;

  if ( direction == 'up' ) {
    value = 1;
  }

  if ( direction == 'down' ) {
    value = -1;
  }

  // assemble vote object to be recorded in votes collection
  const voteObj: Vote = { username: username, recipeId: recipeId , value: value };

  // get references to both vote and recipe documents
  const voteDocRef = this.afs.doc(`votes/${username}_${recipeId}`).ref;
  const recipeDocRef = this.afs.doc('recipes/' + recipeId).ref;

  await this.afs.firestore.runTransaction( async t => {

    const voteDoc = await t.get(voteDocRef);
    const recipeDoc = await t.get(recipeDocRef);
    const currentRecipeScore = await recipeDoc.get('score');

    if (!voteDoc.exists) {

      // This is a new vote, so add it to the votes collection
      // and apply its value to the recipe's score
      t.set(voteDocRef, voteObj);
      t.update(recipeDocRef, { score: (currentRecipeScore + value) });

    } else {

      const voteData = voteDoc.data();

      if ( voteData.value == value ) {

        // existing vote is the same as the button that was pressed, so delete
        // the vote document and revert the vote from the recipe's score
        t.delete(voteDocRef);
        t.update(recipeDocRef, { score: (currentRecipeScore - value) });

      } else {

        // existing vote is the opposite of the one pressed, so update the
        // vote doc, then apply it to the recipe's score by doubling it.
        // For example, if the current score is 1 and the user reverses their
        // +1 vote by pressing -1, we apply -2 so the score will become -1.
        t.set(voteDocRef, voteObj);
        t.update(recipeDocRef, { score: (currentRecipeScore + (value*2))});
      }

    }

    return Promise.resolve(true);

  });

}
Derrick Miller
  • 1,860
  • 3
  • 21
  • 37
0

I was facing the same problem and decided to use a combination of a batched write and "normal" reads. The decision was guided by the fact that I needed to make many reads that did not rely on each other. At first I used a method similar to the one proposed by Derrick above, but it proved not sustainable for may reads. The code dictates that every loop is blocking to the next one. What I did was to batch all the reads to run in parallel with Promise.all The disadvantage of this is that you dont take advantage of transaction features, but since the field I was iterested in was not changing, it made sense Here's my sample code

const batch = firestore().batch()
 const readPromises = invoiceValues.map(val => {
                        return orderCollection(omcId).where(<query field>, '<query operation>', <query>).get()
                    })

                    return Promise.all(readPromises).then(orderDocs => {
                  //Perform batch operations here
                        return batch.commit()
                     })

This has proven to be more efficient for many reads, while remaining safe since the fields I'm interested in dont change

Kisinga
  • 1,640
  • 18
  • 27
  • Will this lock data while reading? If NOT, it will cause concurrency issues? – beedrill Jun 19 '20 at 03:48
  • What do you mean by "Lock data"? Because I belive locking happens at the db level. All the operations are independent of each other, so in case there are collisions when writing, only the lasp operation will persist. Therefore, this code is only safe if the query is guaranteed to produce unique results in each read and there is no "repeating value" – Kisinga Jun 19 '20 at 14:28
  • In my understanding. What happens in a transaction is that: you read the data first and then write to the database, a transaction will guarantee the data will not change after you read and before you write, by applying a certain 'lock'. In this way, you are confident that you are reading the newest information until the update is done. In this example though, such data consistency can't be guaranteed? What if someone else performed an update right after your 'get' method? – beedrill Jun 20 '20 at 02:59
  • You're absolutely right, and no, this is not a transaction. I had not understood that part. The only scenario this code can be applied is where every operation is indempotent. – Kisinga Jun 21 '20 at 19:20