I am quite new to Firebase and NoSQL databases and I am developing server-side.
My goal is to read two collections in one transaction (and lock the included documents for consistency). On the one hand I want to read up to 499 documents from collection A and on the other hand exactly one document from collection B, as illustrated in the following code example.
export const transactionTest = function(nextFunction: () => void) {
const collection_A_reference = admin.firestore().collection("A").limit(499);
const collection_B_doc_B1_reference = admin.firestore().collection("B").doc("B1");
try {
admin.firestore().runTransaction((t) => {
return t.get(collection_A_reference)
.then((coll_A_snapshot) => {
if (!(coll_A_snapshot.empty)) {
t.get(collection_B_doc_B1_reference)
.then((coll_B_doc_B1_snapshot) => {
if (coll_B_doc_B1_snapshot.exists) {
let counter = coll_B_doc_B1_snapshot.get("COUNTER");
for (let i = 0; i < coll_A_snapshot.docs.length; i ) {
counter ;
t.update(coll_A_snapshot.docs[i].ref, {COUNTER: counter});
}
t.update(coll_B_doc_B1_snapshot.ref, {COUNTER: counter});
} else {
console.log("coll_B_doc_B1_snapshot does not exist");
}
});
} else {
console.log("coll_A_snapshot is empty");
}
});
});
} catch (e) {
console.log("Transaction failure:", e);
nextFunction();
}
nextFunction();
};
However, it seems like a second t.get is not allowed and is throwing the following error:
(node:13460) UnhandledPromiseRejectionWarning: Error: 10 ABORTED: The referenced transaction has expired or is no longer valid
Does someone know how to implement this (especially syntactically)? I googled a lot but I did not quite find what I wanted. Maybe I also have an error in reasoning here on how to use transactions in firebase. One workaround might be to create an array of DocumentReference
s before the transaction and then use it transaction.getAll()
but that does not appear very elegant.
I will be thankful for any help :)
Best regards
CodePudding user response:
The problem isn't that multiple Transaction#get()
calls is being made, it's that you haven't chained the promises properly. This causes the transaction to complete (by doing nothing) and you get the "The referenced transaction has expired" error message.
admin.firestore().runTransaction((t) => {
return t.get(collection_A_reference)
.then((coll_A_snapshot) => {
if (!(coll_A_snapshot.empty)) {
return t.get(collection_B_doc_B1_reference) // <-- was missing this return
.then((coll_B_doc_B1_snapshot) => {
if (coll_B_doc_B1_snapshot.exists) {
let counter = coll_B_doc_B1_snapshot.get("COUNTER");
for (let i = 0; i < coll_A_snapshot.docs.length; i ) {
counter ;
t.update(coll_A_snapshot.docs[i].ref, {COUNTER: counter});
}
t.update(coll_B_doc_B1_snapshot.ref, {COUNTER: counter});
} else {
console.log("coll_B_doc_B1_snapshot does not exist");
}
});
} else {
console.log("coll_A_snapshot is empty");
}
});
});
Notes:
- The body of a Transaction (anything inside the
(t) => { /* ... */ }
) may be reattempted many times. So be careful doing things like logging as you could end up with lots of logs that have data that was ignored. Instead you should return information about the result. If the transaction needs to be reattempted, the object is discarded and replaced with the result of the next attempt. When the transaction succeeds, the latest object is passed back to the caller and then based on that object, do your logging. The only useful log inside the transaction would be to keep track of attempts and how long they are taking. - Unless you are using
await
with a Promise, surrounding it with atry
/catch
block is pointless.
try {
await admin.firestore().runTransaction((t) => { /* ... */ });
} catch (e) {
console.log("Transaction failure:", e);
nextFunction();
}
In terms of refactoring it, you could go with:
const transactionResult = await admin.firestore()
.runTransaction(async (t) => {
const coll_B_doc_B1_snapshot = await t.get(collection_B_doc_B1_reference); // check B1 first (only reading 1 document instead of up to 499)
if (!coll_B_doc_B1_snapshot.exists)
return { aborted: true, type: "missing-doc" };
const coll_A_snapshot = await t.get(collection_A_reference);
if (coll_A_snapshot.empty)
return { aborted: true, type: "empty" };
// queue changes
let counter = coll_B_doc_B1_snapshot.get("COUNTER");
for (let i = 0; i < coll_A_snapshot.docs.length; i ) {
counter ; // <- is this meant to increment on every loop?
t.update(coll_A_snapshot.docs[i].ref, {COUNTER: counter});
}
t.update(coll_B_doc_B1_snapshot.ref, {COUNTER: counter});
return { aborted: false, counter };
})
.catch(error => ({ aborted: true, type: "error", error }));
if (transactionResult.aborted) {
if (transactionResult.type === "empty") {
console.error(`Update failed, because coll_A is empty`);
} else if (transactionResult.type === "missing-doc") {
console.error(`Update failed, because coll_B_doc_B1 is missing`);
} else {
console.error("Update failed, because of ${transactionResult.type}", transactionResult.error);
}
nextFunction();
return;
}
console.log(`Counter was successfully updated. B1 now has a COUNTER of ${transactionResult.counter}`);
nextFunction();