Wednesday, June 9, 2021

Firebase FIrestore - tips and tricks

0_Unsorted_

Hereinafter code examples are in Kotlin (not Java).

Storage, documents and collections

Let's distinguish all the terms in three groups: storage, documents and collections.
Storage is the mostly memory consuming part of database. Any data submitted by the user is stored in the storage. Whether it is text, picture or geo location. In the code, we don't call the storage directly. Instead, we use documents to reference it.
Documents present hash maps to reference the storage. Hash map is needed because the document has multiple fields and each field should have a reference to the value.
Collections are merely tags to combine documents into groups. So if we would not have collections initially, we could easily create them. For that, just create a new "collection" field in the document and place there some collection name. So all the documents with that value will be combined into the same collection group. 
That's why we cannot create subcollection inside of the collection. We can create subcollection only inside of the document. Because the document can easily reference anything in its hash, whether it is value in storage, name of the collection etc. Collection cannot reference anything because the collection per se is merely the name of the tag which is referenced by the document.
For the same reason, multiple documents in the same collection (though it would be proper to say "of the same collection") can contain different fields with different types of values. Collection does not restrict the document. Instead, it just places a marker on the document, it marks the document with some kind of tag and nothing else.
Say we have multiple dogs in the yard. Some of them are white and some are not. Our task is to call all the white dogs and gather them for a walk to the park. In a regular database, dogs don't want to listen to our commands. So we need to build up a crate called "table of white dogs" and lock all the white dogs into that crate. So we don't need to spend time for searching for dogs of particular color.
In NoSQL database like Firebase, dogs are smarter and they can listen to our commands. Every white dog has a "white" tag on its collar. Instead of being locked and crowded in the crate, they can freely walk in any free area of the yard. When we need them, we just call "white dogs come here!". And all the white dogs run with us for a walk to the park.

Difference between retrieving and referencing

The database distinguishes retrieving operation from reverencing it. Retrieving means actually getting data by calling the remote server and retrieving real values of records. Unlike retrieving, referencing operation does not call the Firebase server. It just saved address of data into certain variable. That variable can be used in the future for actual retrieving. For the purposes of this post, I consider adding data under the retrieving section also, because adding data actually connects to the database, not merely creating a reference.
So in terms of postal office, referencing is like writing an address on an envelope. And retrieving is actually mailing an envelope and getting a response.

Retrieving

Add data to Firestore collection

Credits to native Firebase manual
val data1 = hashMapOf(
"name" to "San Francisco",
"state" to "CA",
"country" to "USA",
"capital" to false,
"population" to 860000,
"regions" to listOf("west_coast", "norcal")
)
cities.document("SF").set(data1)

Retrieve filtered collection

db.collection("cities")
.whereEqualTo("capital", true)
.get()
.addOnSuccessListener { documents ->
for (document in documents) {
Log.d(TAG, "${document.id} => ${document.data}")
}
}
.addOnFailureListener { exception ->
Log.w(TAG, "Error getting documents: ", exception)
}

Retrieve document and its field values

The trick is we need to use addOnSuccessListener to retrieve the field value. So retrieving from the remote database is launched in separate flow. Then, if we want to use this value right in the next line of code, it could raise a null error if the value didn't come from the database yet.
Possible solutions are the following:
1) Place the next line of code (where we use the value to be retrieved) right inside the brackets of the onSuccessListener flow:
request.get()
.addOnSuccessListener { documentSnapshot ->
val userCreatedId = documentSnapshot.get("user_created_id") as String
request.update(mapOf(
user_responded_idLabel to currentUser?.uid
)
)
}

2) Store data offline beforehand. E. g. if the major part of the app is working around the same request, it makes sense to create an offline hash of the request object as soon as possible, in the beginning of the user session. Then just don't forget to update the hash if the request fields values change.

3) Retrieve data to custom object. There are 2 notes here:

- The object class should be with "data prefix"
- Official manual suggests us to use "toObject<Class>" function, however the compiler yells with an error. Instead, we use toObject(MyClassName::class.java) even though MyClassName is defined as Kotlin class.. See also this stackoverflow answer.
None of the following functions can be called with the arguments supplied: 
public open fun <T : Any!> toObject(p0: Class<TypeVariable(T)!>): TypeVariable(T)? defined in com.google.firebase.firestore.DocumentSnapshot
So the working code is as follows:
data class Request(
val id: String? = null,
val description: String? = null,
val user_created_id: String? = null,
val user_responded_id: String? = null,
val status: String? = null
)

val docRef = Request.COLLECTION_REFERENCE.document("hDsbP2yW4jIC40mUE13z")
docRef.get().addOnSuccessListener { documentSnapshot ->
val request = documentSnapshot.toObject(
Request::class.java)
}

Referencing

Reference filtered collection

Credits to native Firebase manual.

citiesRef.whereNotEqualTo("capital", false)

Reference certain document

First way to reference the document:
val alovelaceDocumentRef = db.collection("users").document("alovelace")
Another way to reference the document:
val alovelaceDocumentRef = db.document("users/alovelace")

Async, coroutines, await etc

await() is not recognized

If await() keyword is not recognized, importing the following library could help:

implementation 'com.google.android.gms:play-services-tasks:9.6.1'


No comments:

Post a Comment