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');
});
});
There are three ways to retrieve data stored in Cloud Firestore. Any of these methods can be used with documents, collections of documents, or the results of queries: Call a method to get the data once. Set a listener to receive data-change events.
In the case of a concurrent edit, Cloud Firestore runs the entire transaction again. For example, if a transaction reads documents and another client modifies any of those documents, Cloud Firestore retries the transaction. This feature ensures that the transaction runs on up-to-date and consistent data.
Firestore offers two types of batch operations – transactions (which allow both reads and writes) and batched writes (for writes only). However, both of those however have hard limit of writing to 500 documents.
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'));
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);
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With