Firestore transaction with multiple get

2020-06-09 03:30发布

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');
         });
    });

3条回答
Luminary・发光体
2楼-- · 2020-06-09 03:59

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

查看更多
放荡不羁爱自由
3楼-- · 2020-06-09 04:02

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);

  });

}
查看更多
够拽才男人
4楼-- · 2020-06-09 04:07

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'));
查看更多
登录 后发表回答