Flutter Firebase/Firestore write to multiple tables at once - firebase

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.

Related

Firebase Firestore - Best way to call a bunch of where gets?

I have an array of user emails that I want to then pull from firebase each of the corresponding user documents from a collection where one of the email matches. I don't want to pull the whole collection as it would be expensive. What is the easiest way to do this? I tried for looping over the array with individual gets to firebase but ran into promise issues as I want to do something in Javascript with them right after.
Thanks so much!
based on what i understood from your question i can only think of using await Promise.all() - you can look into this here.
as an example you could pass an array of promises to await Promise.all() so you could do
const res = await Promise.all(
array.map( x => db.collection('users')
.where('email' , '==', x.email).limit(1).get());
mind you that in this example you would still have to process the result as they will return a snapshot not a document ...
Update:
Hey there, i just noticed that you can use in operator in firebase query, which will return the values matching in a given array.
i'm not sure but maybe using it might be suitable in your use-case you can check the documentation here

How to avoid double Cloud Firestore write when uploading Storage file using the document ID and passing the DownloadURL

I am working on a Flutter app connected to Firebase Cloud Firestore and Storage, and I want to create documents in Cloud Firestore that contain the downloadURL of files in Storage. However, I also need the filename to be the unique ID of the same Firestore document. Right now I upload the file after I create the document, but it would cost me an extra write to update the Firestore document afterwards.
I want to prevent this. It might work if I could access the id of the newly created document within the add() function, so that I can pass it to the storageTask, which would return the downloadURL directly inside the add() function. Another option would be if I could create a document, get its ID, then do the upload task, and write all the data afterwards, but I''m not sure if this would count as one write or two, nor do I know how to create a document without setting any data.
What I have now is roughly like this:
CollectionReference activities = FirebaseFirestore.instance.collection('activities');
activities.add({
'postDate': DateTime.now(),
}).then((value) => _startUpload(id: value.id));
Where _startUpload is a function that uploads a file to Storage, and could potentially return a downloadURL. I want to be able to access that URL in the add function like this:
CollectionReference activities = FirebaseFirestore.instance.collection('activities');
activities.add({
'postDate': DateTime.now(),
'downloadURL': _startUpload(id: SomehowGetThisDocuments.id)
});
You can do something like described in the last paragraph of this section in the documentation:
newActivityRef = activities.document();
newActivityRef.set({
'postDate': DateTime.now(),
'downloadURL': _startUpload(id: newActivityRef.documentID)
});
This should count as one write as .document().set() is equivalent to .add() according to the documentation.

How to ensure the FieldValue.serverTimestamp() finish executing in Cloud Firestore before I get the latest data on my listener?

I'm coding in Flutter and I'm updating a document in my Cloud Firestore
// Prepare the data
final Map<String, dynamic> data = {
'name': newName,
'lastUpdate': FieldValue.serverTimestamp()
};
// Update the data in Firestore
await documentReference.updateData(data);
I also have a subscription that listens to that documentReference like this:
subscription = documentReference
.snapshots().listen((){});
subscription.onData(printSnapshotData);
What happen is I only update the data once but I will get two data reads on my listener.
The first one is like this:
name: 'newName'
lastUpdated: null
The second one is like this:
name: 'newName'
lastUpdated: Timestamp(seconds=1594368407, nanoseconds=376000000)
So it seems that there was a delay between the first one and the second one and the app gets the data twice.
What I want is to get the data after the Timestamp in the lastUpdated field is done. Only read once is enough because the first data is useless for me. How to do that ?
Thank you!
The fact that you are getting two updates on your listener is expected, and that behavior can't be changed. Listeners always fire immediately with local changes, then again every time the data has changed. Since the SDK doesn't know the final value of the timestamp before it's written, it will invoke once without the final value, then again with the final server timestamp. The first callback doesn't cost any document reads - it is working purely with local data.
If you want to know if the snapshot delivered to your callback comes from the server, you will have to check the data in the snapshot to see if it contains what you need.
For more detailed information about how server timestamps work, read this blog post.
Further to Doug extremely informative answer. I wanted to add that it is possible to use the snapshots metadata's hasPendingWrites property to filter out the local updates before the timestamp is added. See example below.
subscription = documentReference
.snapshots()
.where((snapshot) => !snapshot.metadata.hasPendingWrites)
.listen((snapshot){
// Your code here
});

What is the difference between getDocuments() and snapshots() in Firestore?

I am a little confused about the difference between these two. My understanding is that getDocuments is a type of Future and seems to get the entire documents according to the query. while snapshots, on the other hand, is a type of Stream and, correct me if I'm wrong, I think it represents the results of the query? I need a more specific explanation of this issue. I will include some code snippets as an example for more clarification
getDocuments()
getUserById(String userId) async {
return await _firestore.collection("users").where("userId", isEqualTo: userId).getDocuments();
}
snapshots()
getUserById(String userId) async {
return await _firestore.collection("users").where("userId", isEqualTo: userId).snapshots();
}
So what's the difference?
When you call getDocuments(), the Firestore client gets the documents matching the query from the server once. Since this may take some time it returns a Future<QuerySnapshot>.
When you call snapshots() the Firestore client gets the documents, and then keeps watching the database on the server for changes that affect your query. So if document is written in the users collection that affects your query, your code gets called again. So this returns a stream of QuerySnapshot.
In both cases the results for the entire query are in the QuerySnapshot object.
I highly recommend reading the Firestore documentation on getting data once and on listening realtime updates. While they don't contain Flutter examples, the explanation in there applies equally to the Flutter libraries.
getDocuments():
It's used to provide data once. Cloud Firestore contains collections and inside these collections, you have documents that may contain subcollections or fields mapped to a value. To retrieve any of the doc fields to used it in widget this is used.
snapshots():
It will be called on every data change in your document query. For this StreamBuilder must be used to fetch fields as modified.
In short, it will do the job of setState() where it gives you the response for every modification so that UI can be updated.

Firestore BatchWrite vs Transactions

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

Resources