Firestore BatchWrite vs Transactions - firebase

After following this firestore document, it's pretty clear that both batch writes and transactions are a great for grouping multiple operations.
Although i couldn't figure what would be the best practice for my use case.
This is my batch write:
val batch = db.batch()
val docRef = db.collection(CHATS_COLLECTION).document()
chat.id = docRef.id
return Observable.create { emitter: ObservableEmitter<String> ->
batch.set(docRef, chat)
batch.update(db.collection(USERS_COLLECTION).document(userId), "chatIds",
FieldValue.arrayUnion(db.document("$CHATS_COLLECTION/${docRef.id}")))
batch.commit()
.addOnSuccessListener {
emitter.onNext(docRef.id)
emitter.onComplete()
}
.addOnFailureListener { e ->
emitter.onError(e)
}
}
I'm creating new chat document and at the same time updating my user document "chatsIds" array with the newly created document using batch write.
What could be drawbacks in replacing batch write with transaction?

It would get slower without any added benefit. Batched writes are perfect for your case: you want to update multiple places in your database without any reads.
If you had reads, then it would make sense to use a transaction. In your case, a batch write is the correct choice simply because it runs faster.

Both batch and transactions are will execute atomic manner.Batch write is suitable if you are not reading data from document while updating the document and transaction will be suitable if there is a reading before updating the document

Related

How to limit size of an array in Firestore on a write?

Does anyone know how to limit an array so new items get pushed in and old ones are discarded in the same write?
I'm guessing this isn't possible but it sure would be handy.
// * Store notification
// Users collection
const usersCollection = db.collection('users').doc(uid).collection('notifications').doc();
// Write this notification to the database as well
await usersCollection.update({
count: admin.firestore.FieldValue.increment,
notifications: admin.firestore.FieldValue.arrayUnion({
'symbol': symbol,
'companyname': companyname,
'change': priceDifference,
'changeDirection': directionOperatorHandler,
'updatedPrice': symbolLatestPrice,
'timestamp': currentTimestamp,
})
});
Written in Typescript
Alternatively, I was thinking of running a scheduled cloud function every week to go through and trim down the arrays based on the timestamp.
The reason I'm using an array to store my notifications is because I'm expecting a lot of writes.
There is no simple configuration for this. Your code should implement your requirements by:
Reading the document
Modifying the array in memory
Checking that the size is within limits
Writing the document back

Flutter Firebase/Firestore write to multiple tables at once

I'm using Firebase as backend to my Flutter project. I need to write to multiple tables in one transaction. Now I have:
await _databaseReference
.collection('user_data')
.document(uid)
.setData(userData);
await _databaseReference
.collection('company_data')
.document(uid)
.setData(companyData);
But this these are 2 transactions, so 1 could ends successfully but another one could fail and I want to prevent to happening this.
Thanks a lot.
You have to use a Batched Write, which "is a series of write operations to be performed as one unit", as follows:
var batch = _databaseReference.batch();
batch.setData(
_databaseReference.collection('user_data').document(uid),
userData
);
batch.setData(
_databaseReference.collection('company_data').document(uid),
companyData
);
await batch.commit();
Following Rahul Vyas comment below, note that a batch can contain, at the same time, some set, update and delete operations to be executed on different documents (potentially in different collections, as shown above). Look at the Methods section in the doc.

How to update the same document with a read from the same collection in an onUpdate function

I'm trying to update the same document which triggered an onUpdate cloud function, with a read value from the same collection.
This is in a kind of chat app made in Flutter, where the previous response to an inquiry is replicated to the document now being updated, for easier showing in the app.
The code does work, however when a user quickly responds to two separate inquiries, they both read the same latest response thus setting the same previousResponse. This must be down to the asynchronous nature of flutter and/or the cloud function, but I can't figure out where to await or if there's a better way to make the function, so it is never triggering the onUpdate for the same user, until a previous trigger is finished.
Last part also sound a bit like a bad idea.
So far I tried sticking the read/update in a transaction, however that only seems to work for the single function call, and not when they're asynchronous.
Also figured I could fix it, by reading the previous response in a transaction on the client, however firebase doesn't allow reading from a collection in a transaction, when not using the server API.
async function setPreviousResponseToInquiry(
senderUid: string,
recipientUid: string,
inquiryId: string) {
return admin.firestore().collection('/inquiries')
.where('recipientUid', '==', recipientUid)
.where('senderUid', '==', senderUid)
.where('responded', '==', true)
.orderBy('modified', 'desc')
.limit(2)
.get().then(snapshot => {
if (!snapshot.empty &&
snapshot.docs.length >= 2) {
return admin.firestore()
.doc(`/inquiries/${inquiryId}`)
.get().then(snap => {
return snap.ref.update({
previousResponse: snapshot.docs[1].data().response
})
})
}
})
}
I see three possible solutions:
Use a transaction on the server, which ensures that the update you write must be based on the version of the data you read. If the value you write depends on the data that trigger the Cloud Function, you may need to re-read that data as part of the transaction.
Don't use Cloud Functions, but run all updates from the client. This allows you to use transactions to prevent the race condition.
If it's no possible to use a transaction, you may have to include a custom version number in both the upstream data (the data that triggers the write), and the fanned out data that you're updating. You can then use security rules to ensure that the downstream data can only be written if its version matches the current upstream data.
I'd consider/try them in the above order, as they gradually get more involved.

Firestore default users data initialization

I am creating a game and I want to store completed game levels on firestore for each user.
Now my problem is that I will have to initalize this data once - I want to add a document for new user and a pojo that containts map of level ids and boolean for completed/uncompleted.
So I need to execute some kind of logic like "if document with this id doesnt exist, then add that document and add default data that means user hasnt completed any levels". Is there some way that would guarantee Id have to execute this logic only once? I want to avoid some kind of repeating/re-try if something fails and so on, thanks for your suggestion
That's what a transaction is for (definitely read the linked docs). In your transaction, you can read the document to find out if it exists, then write the document if it does not.
Alternatively, you may be able to get away with a set() with merge. A merged set operation will create the document if it doesn't exist, then update the document with the data you specify.
The typical approach to create-a-document-if-it-doesn't-exist-yet is to use a transaction. Based on the sample code in the documentation on transactions:
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(sfDocRef).then(function(sfDoc) {
if (!sfDoc.exists) {
transaction.set(sfDocRef, { count: 1 });
}
});
}).then(function() {
console.log("Transaction successfully committed!");
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
Also see:
Firestore create document if it doesn't exist security rule, which shows security rules that allow a document to be create-but-not-updated.
Create a document only if it doesn't exist in Firebase Firestore, which shows how to allow a document only to be created by a UID identified in the data.
the documentation on transactions
the reference docs for the Transaction class

Removing all documents and collections from the Firestore

I am trying to clear a Firestore database which was filled with a lot of documents and subcollections for testing purposes. The Firebase CLI (firebase-tools#3.18.4) suggests the following possibility to delete data from Cloud Firestore:
Usage: firestore:delete [options] [path]
Options:
-r, --recursive Recursive. Delete all documents and subcollections. Any action which would result in the deletion of child documents will fail if this argument is not passed. May not be passed along with --shallow.
--shallow Shallow. Delete only parent documents and ignore documents in subcollections. Any action which would orphan documents will fail if this argument is not passed. May not be passed along with -r.
--all-collections Delete all. Deletes the entire Firestore database, including all collections and documents. Any other flags or arguments will be ignored.
-y, --yes No confirmation. Otherwise, a confirmation prompt will appear.
The problem is that it does not really work for me.
Executing firebase firestore:delete --all-collections produces the following output:
You are about to delete YOUR ENTIRE DATABASE. Are you sure? Yes
Deleting the following collections: 13OPlWrRit5PoaAbM0Rk, 17lHmJpTKVn1MBBbC169, 18LvlhhaCA1tygJYqIDt, 1DgDspzJwSEZrYxeM5G6, 1GQE7ySki4MhXxAeAzpx, 1MhoDe5JZY8Lz3yd7rVl, 1NOZ7OJeqSKl38dyh5Sw, 1Rxkjpgmr3gKvYhBJX29, 1S3mAhzQMd137Eli7qAp, 1S8FZxuefpIWBGx0hJW2, 1a7viEplYa79eNNus5xC, 1cgzMxAayzSkZv2iZf6e, 1dGjESrw6j12hEOqMpky, 1dbfgFD5teTXvQ6Ym897, 1eeYQgv2BJIS0aFWPksD, 1ehWNAZ0uKwg7mPXt3go, 1fDTkbwrXmGwZlFUl3zi, 1k5bk4aiMCuPw2KvCoAl, 1pxUSDh1YqkQAcuUH9Ie, 1rMSZ5Ru0cAfdcjY0Ljy
Deleted 92 docs (652 docs/s)
Even after executing the function multiple times an awful lot of documents and subcollections still remain in the Firestore database. Instead of deleting the ENTIRE DATABASE, only about 70-150 documents are deleted when the command is executed.
How can the entire database be deleted?
I've reported this as a bug and received the following answer:
Currently, this is an intended behavior. As stated in our documentation, deleting a collection of more than 500 documents requires multiple batched operations. So doing the iteration would be a good way to handle cases of partial deletion. I would also suggest that you check our docs regarding some of the callable function limitations for more details.
This means that firebase-tools always deletes a maximum of 500 documents in one operation. My solution to delete all collections and documents in the database is to use a while loop:
while firebase firestore:delete --all-collections --project MYPROJECT -y; do :; done
After some iterations you will see that there are no collections left and you can stop the script. Your Firestore DB is now completely empty.
You will want to use the admin sdk for this task. Use .listDocuments and .listCollections to build out simple iterations to perform your .delete operation(s) against.
If a documents .listCollections response is zero in length or null or empty, you know there's no subcollections and can iterate on / skip. Else, iterate that subcollections documents looking for deeper subcollections to delete.
let documentRef = firestore.doc('col/doc');
documentRef.listCollections().then(collections => {
for (let collection of collections) {
console.log(`Found subcollection with id: ${collection.id}`);
}
});
and
let collectionRef = firestore.collection('col');
return collectionRef.listDocuments().then(documentRefs => {
return firestore.getAll(documentRefs);
}).then(documentSnapshots => {
for (let documentSnapshot of documentSnapshots) {
if (documentSnapshot.exists) {
console.log(`Found document with data: ${documentSnapshot.id}`);
} else {
console.log(`Found missing document: ${documentSnapshot.id}`);
}
}
});

Resources