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

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.

Related

Flutter Firebase local change doesn't update listener stream

I'm relying on Firebase Firestore offline capabilities, so I'm not using await on my queries as stated on the Access Data Offline Firebase doc. I'm expecting that when I write something I'll get an immediate reflection on my read stream, however, I'm only getting an update when the server/remote has been updated. Basically:
Update something in the DB. Note, I'm not using await
_db.doc(parentDoc).collection(DocInnerCollection).doc(childDoc).update({
"name": value,
});
I expect my listeners to be updated immediately. Note I've set the includeMetadataChanges to true as stated in the above doc.
_db.doc(parentDoc)
.collection(DocInnerCollection)
.orderBy('start_date', 'desc')
.limitToLast(1)
.snapshots(includeMetadataChanges: true)
.map((snapshot) {
print(snapshot.metadata.isFromCache)
});
However, I get no such update and instead I only get an update when the server has been updated.
You're requesting only one document with .limitToLast(1), yet are not providing a sort order for your query. This essentially means that you'll get a random document from your collection, and the chances of that being the newly updated document are close to zero.
If you want the latest (not just last) document, you need some ordering criteria to determine what latest means. Typically you'd do this by:
Adding a lastUpdated field to your documents, and setting that to firebase.firestore.FieldValue.serverTimestamp().
Ordering your query on that timestamp with orderBy('lastUpdated', 'desc').
And then limiting to the first result with limit(1).

What is the read count for realtime listening of cloud_firestore query and can it be done better?

Imagine I have the following pseudo code in flutter/dart :
Stream<List<T>> list() {
Query query = Firestore.instance.collection("items");
return query.snapshots().map((snapshot) {
return snapshot.documents.map((doc) {
return standardSerializers.deserializeWith(serializer, doc.data());
}).toList();
});
}
I am listening to the whole collection of "items" in my database. Let's say for simplicity there are 10 documents in total and I constantly listen for changes.
I listen to this stream somewhere in my code. Let's say this query returns all 10 "items" the first time this is called for example. This counts as 10 reads, ok fine. If I modify one of these documents directly in the firestore web interface (or elsewhere), the listener is fired and I have the impression another 10 reads are counted, even though I only modified one document. I checked in the usage tab of my cloud project and I have this suspicion.
Is this the case that 10 document reads are counted even if just one document is modified for this query?
If the answer is yes, the next question would be "Imagine I wanted to have two calls to list(), one with orderBy "rating", another with orderBy "time" (random attributes), one of these documents changes, this would mean 20 reads for 1 update"?
Either I am missing something or firestore isn't adapted for my use or I should change my architecture or I miscounted.
Is there any way to just retrieve the changed documents? (I can obviously implement a cache, local db, and timestamp system to avoid useless reads if firestore does not do this)
pubspec.yaml =>
firebase_database: ^4.0.0
firebase_auth: ^0.18.0+1
cloud_firestore: ^0.14.0+2
This probably applies to all envs like iOS and Android as it is essentially a more general "firestore" question, but example in flutter/dart as that is what I am using just in case it has something to do with the flutterfire plugin.
Thank you in advance.
Q1: Is this the case that 10 document reads are counted even if just one document is modified for this query?
No, as detailed in the documentation:
When you listen to the results of a query [Note: (or a collection or subcollection)], you are charged for a read
each time a document in the result set is added or updated. You are
also charged for a read when a document is removed from the result set
because the document has changed. (In contrast, when a document is
deleted, you are not charged for a read.)
Also, if the listener is disconnected for more than 30 minutes (for
example, if the user goes offline), you will be charged for reads as
if you had issued a brand-new query. [Note: So 10 reads in your example.]
Q2: If the answer is yes, the next question...
The answer to Q1 is "no" :-)
Q3: Is there any way to just retrieve the changed documents?
Yes, see this part of the doc, which explains how to catch the actual changes to query results between query snapshots, instead of simply using the entire query snapshot. For Flutter you should use the docChanges property.

The method 'updateData' isn't defined for the type 'Query'. Futter, Cloud Firestore

I would like to update data in a document based on a query. Here is my code:
Firestore.instance
.collection("Categories")
.where("userEmail", isEqualTo: "${user?.email}")
.updateData({"category_budget_remaining": _remainingCategoryBudget2})
.then((value){});
I am getting the following error: The method 'updateData' isn't defined for the type 'Query'
after where() you need to get documents then do updating
where(...).getDocuments().then((val)=>
val.documents.forEach((doc)=> {
doc.reference.updateData({...})
});
});
this code selects and updates all documents that goes true for where condition. If you want do it just for 1 document then just add .limitTo(1) before getDocuments().
As you can see from the API documentation, where() returns a Query, and Query doesn't have a method called updateData(). So, what you're seeing here is no surprise.
Firestore doesn't offer a way to bulk update documents like a SQL "update where" command. What you will have to do is execute the query for the documents to change, iterate the documents in the result set, and update each one individually. Yes, it requires a document read for every document to change, and no, there are no alternatives for this.
For me worked by changing
.updateData
to just
.update
My answer is similar to #Habib Mahamadi but this is the correct and only way to acheive this functionality, my answer is little differente as firebase has changed names of functions.
Firestore.instance
.collection("Categories")
.where("userEmail", isEqualTo: "${user?.email}")
.then((value)=> value.docs.forEach((element){element.reference.update({"" : ""})});

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.

When is data actually transferred in firestore? In the document snapshot? Or not until .data() or .get() is called?

I'm trying to decide how to structure permissions for an app. Much of this comes down to the question in the title. Specifically, if I call:
const tasks = db.collection('tasks').where('canRead', 'array-contains', firebase.auth().currentUser.uid).get() and retrieve an array of document snapshots, is the data actually transferred for each snapshot, or is the data not transferred until I iterate through that array:
for(let taskSS of tasks.docs) { ... and eventually call .data() with let task = taskSS.data() ?
The reason I ask is if I had a task document with a massive array of users who are allowed to get and list that task, I would not want to transfer all of that data on the initial db.collection('tasks').where('canRead', 'array-contains', firebase.auth().currentUser.uid).get() call.
If the data is not actually transferred until we call either .data() or .get() then I could put all the task data in task.public and then just do let task = taskSS.get('public').
Does someone know when the actual data is transferred and put into memory with firestore? Is the data actually "in" the document snapshot or not transferred until you request that data with .data() or .get()?
The data is transferred from the database to your application when you call get() or onSnapshot(). Each DocumentSnapshot contains all data for the document when you get it. Calling document.data() merely returns a map of the data it already has.

Resources