Flutter Firebase local change doesn't update listener stream - firebase

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).

Related

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.

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.

Why does Firestore send all results, instead of just the new items when using on_snapshot?

I'm using Firebase's Firestore to store and publish new events.
In the code below, I'm subscribing to a collection and want to be notified when a new items is added (this code is executing on a browser).
When I first connect, I would like to receive a true snapshot. However, once I'm connected to Firestore and have received an initial snapshot, with each new item, I only want to get the udpates, not the whole collection over and over again!
function queryExercise(exercise){
db.collection("exercises").where("exercise","==",exercise).onSnapshot(function(querySnapshot){
querySnapshot.forEach(function(doc){
var d_ = doc.data()
console.log(d_);
...do somethign with d_...
})
})
}
When I publish a new item to the collection, my console is full of all events received earlier...in other words, it is sending me the full snapshot, instead of just the deltas.
Am I doing something wrong or does the API really not support delta updates?
Looks like I needed to read on docChanges:
function queryExercise(exercise){
db.collection("exercises").where("exercise","==",exercise).onSnapshot(function(querySnapshot){
// \/-----this thing
querySnapshot.docChanges().forEach(function(change){
var d_ = change.doc.data()
console.log("Change type:", change.type, d_);
...
});
})
}
From https://firebase.google.com/docs/firestore/query-data/listen
That's the way Firestore queries work. If you don't provide a filter for which documents you want in a collection, you will get all the documents in that collection. The only way to change this behavior is to provide a filter in your query using a where clause.
It sounds like you have a thought in mind about what makes for a "new" document in your collection. You will need to represent that using some field in the documents in your collection. Usually this will be a timestamp type field that's added or modified whenever a document is created or changed. This will be part of your where clause that determines what's "new". Use this field as a filter to find out what's new.

Why does querying Firebase realtime db require listener?

I am new to firebase and noSQL databases. I've read the docs and watched instructional videos but there is one concept I haven't fully grasped.
Why is it that querying the database requires a listener such as .on("value")?
Since these listeners are triggered whenever there is a change of sorts (nodes created, edited, children created) shouldn't there be a more direct way of getting the data from the db? Such as
ref.orderBy("age"). equalTo(30).get()
A method to just get what's in there at the time he instruction is executed, without having to listen to some sort of event?
In SQL it's not like you have to wait for something to change in your db to make this query work:
SELECT * FROM TABLE WHERE X == Y
PS: I know .once() also exists, but my question is more about: if my db never changed, how would I be able to query it and always get the same query result/snapshot?
You didn't define a platform so I will use this Swift pseudo-code example. Suppose we want to get a list of all users, one time.
let usersRef = my_firebase.child("users")
usersRef.observeSingleEvent(by: .value, with: { snapshot in
//all users are returned to the app in the snapshot
// .value means 'give me everything in the specified node'
// so then we can iterate over the snapshot to get each users data
}
You can call the above code any time to get the list of users and it does not add an observer (listener) to the node. It's a one-shot.
Whereas, if we want to be notified of users that are added
let usersRef = my_firebase.child("users")
usersRef.observe(by: .childAdded, with: { snapshot in
//upon first calling this, each user will be sent to the app in the snapshot
// and after that, any user that's added
}
The above code attaches an observer (listener) to the users node and whenever a user is added, it's provided to the app via the snapshot.
Note the use of .observeSingleEvent vs .observe and .value to get everything in the node vs .childAdded to get a specific node.

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.

Resources