Performance of Chaining Firebase Firestore Query and Document Snapshot Tasks - android

I'm creating an Android app that uses Firebase Firestore to store some data. Since collection group queries are not yet supported in Firestore, in order to build some of the result sets that I need, I have to do a Query followed by multiple DocumentReference.get() calls to achieve the desired end result.
My implementation is as follows:
PlaylistFacade.java
public static Task<QuerySnapshot> GetPlaylistSubscriptionsByOwner(FirebaseFirestore db, #NonNull String ownerUserId)
{
Query playlistRef = db.collection("playlistSubscriptions")
.whereEqualTo("ownerId", ownerUserId);
return playlistRef.get();
}
public static Task<DocumentSnapshot> GetPlaylist(FirebaseFirestore db, #NonNull String playlistId)
{
return db.collection("playlists").document(playlistId).get();
}
PlaylistActivity.java
PlaylistFacade.GetPlaylistSubscriptionsByOwner(mFirestore, this.GetCurrentUser().getUid())
.continueWithTask(new Continuation<QuerySnapshot, Task<List<Task<?>>>>() {
#Override
public Task<List<Task<?>>> then(#NonNull Task<QuerySnapshot> task) throws Exception {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for(DocumentSnapshot doc:task.getResult().getDocuments())
{
PlaylistSubscription ps = doc.toObject(PlaylistSubscription.class);
ps.setId(doc.getId());
tasks.add(PlaylistFacade.GetPlaylist(mFirestore, ps.getPlaylistId()));
}
return Tasks.whenAllComplete(tasks);
}
})
.addOnCompleteListener(this, new OnCompleteListener<List<Task<?>>>() {
#Override
public void onComplete(#NonNull Task<List<Task<?>>> task) {
if(task.isSuccessful()){
List<Task<?>> tasks = task.getResult();
List<Playlist> playLists = new ArrayList<>();
int errorCount = 0;
for(Task<?> docTask : tasks)
{
if(docTask.isSuccessful())
{
DocumentSnapshot ds = (DocumentSnapshot)docTask.getResult();
Playlist pl = ds.toObject(Playlist.class);
pl.setId(ds.getId());
playLists.add(pl);
}
else
{
Crashlytics.logException(docTask.getException());
errorCount++;
}
}
if(errorCount > 0)
Toast.makeText(PlaylistActivity.this, "Encountered " + errorCount + " errors.", Toast.LENGTH_LONG).show();
else
Toast.makeText(PlaylistActivity.this, "Success", Toast.LENGTH_LONG).show();
}
else
{
Crashlytics.logException(task.getException());
Toast.makeText(PlaylistActivity.this, task.getException().getMessage(), Toast.LENGTH_LONG).show();
}
}
});
The above works just fine, but I'm curious if there may be a better way to do it.
My questions are as follows:
Is chaining the Task objects using List<> the ideal approach? Am I giving up any potential performance by using this approach?
Will using the Task chain still allow me to take advantage of Firebase's ability to pipeline requests?
Will my use of Tasks.whenAllComplete() allow me to conditionally accept failure of some or all results, or does Firebase's pipelining cause errors to propagate across requests such that I should really just use Tasks.whenAllSuccess() and avoid the need check success of each individual request?
Response times of my implementation seem fine on small result sets. Am I likely to get better performance as my result set grows if I build my result set in a Cloud Function before returning it to my app instead?
At what complexity of Firestore actions should I really be using an Executor as demonstrated in the DocSnippets.java sample on the deleteCollection(...) function?
Would using a Transaction to bundle requests ever net me any performance gains? Performance implications of doing reads inside a transaction aren't discussed in the documentation.
Any news of when collection group queries will be available in Firestore? In time for Google IO, perhaps?
Thank you for your time.
Other helpful resources for those who end up here with similar questions:
Doug Stevenson's Firebase Blog Post on Task Wiring

Related

Firestore Write Data Missing sometimes

We are using Firebase firestore with our simple grocery delivery app (android) for more than a year. Its super fast and great but it keeps missing data while writing.
Before, without using transactions and batched writes, the problem was much more persistent(about 10/1000 entries). After using the issue has been reduced but not completely gone(about 2/1000 entries).
Scenario:
We have few collections which are set/updated when the product is added namely products,stockand daylogs. When we add/update the product, the transaction writes different data to all these collections. But sometimes(2/1000), the transaction is successful and writes to products and stock but not daylogs.
Here is a simpler version of the code. Although, I don't think it's a code issue because it works fine 99% of the time.
firebasefirestore.runTransaction(new Transaction.Function<String>() {
#Override
public String apply(#NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentReference thisProduct = rootRef.collection("products").document(docId);
DocumentReference thisStock = rootRef.collection("stock").document(docId);
DocumentReference logs = rootRef.collection("daylogs").document(LogDocumentId);
transaction.update(thisProduct, data);
transaction.update(thisStock, data);
transaction.update(logs, data);
String complete = "1";
return complete;
}
}).addOnSuccessListener(new OnSuccessListener<String>() {
#Override
public void onSuccess(String result) {
if (result.equals("1")) {
Toast.makeText(AddProduct.this, "Data Added", Toast.LENGTH_SHORT).show();
}
}
});
Why is this happening? And How can this be solved?
Sorry for my english.
Please help.

Delete all documents in Firestore collection

I made a button that is supposed to let users delete all their data based on their userid, the button has the same purpose like a "delete your account" button, but it is not working. I tried to delete all documents in the collection using this code:
final String userid = FirebaseAuth.getInstance.getCurrentUser().getUid();
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
There is a runtimeExecution : no permission error
com.google.android.gms.tasks.RuntimeExecutionException: com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions.
I set the security rules as below so that no other users could access other users' data.
service cloud.firestore {
match /databases/{database}/documents {
match /main/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Not sure what to do, I thought the security rules could prevent security issues but I think it is causing this permission error?
Thank you in advance.
Update:
After doing some research and read so many other answers on this site about this matter, I used this method and I think it is so simple for a dummy like me but never mentioned on other answers I am not sure why:
To remove a user completely from authentication, I used the Firestore extension called "Delete User Data" which is a great help, it is magical and allows me to remove a user just by using this code:
FirebaseAuth.getInstance().getCurrentUser().delete().addOnSuccessListener(new OnSuccessListener<Void>() {...}
It doesn't only delete user authentication, with just that code, that particular user's data in the firestore and the images in the storage are also gone. It is fantastic!
The important point is that you have to connect the user UID from the authentication and connects the UID to the firestore and the storage.
To know more about how to use the "Delete User Data" Firestore extension(it is still in beta mode), try looking at this great blog tutorial from JORGE VERGARA : Installing the Delete User Data extension
How I set the configuration of the extension to delete a user:
How to use the extension: It is so simple I am so happy that it helps me to delete a user so easily without fuss
This is the storage structure:
The structure of my Firestore:
main(collection)--->userID---->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
--->userID2--->journal(collection)----->journalIDdsvsfbsf
----->journalIDdfvdbgnd
The Authentication structure:
See how the firestore and storage have to be connected with the same user UID so that the Firestore extension could delete a user easily by deleting the authentication.
If you still want to use the old method:
I noticed that you cannot delete a firestore document that contains one or multiple sub-collections with just delete method. For example, if you want to delete a document that contains 3 sub-collections, in order to delete that document(a user in my case), you have to get all the documents in one of the sub-collection, delete them all, then proceed to do the same to all the other sub-collections in that document. Once all the sub-collections in the document has been deleted, then the document that contains them will be gone itself. That's how Firestore works it seems.
To put it in code:
to delete a document that contains a sub-collection "journal"
Without using the extension, I have to do it like this:
FirebaseFirestore.getInstance().collection("main").document(userid).collection("journal").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (final QueryDocumentSnapshot document : task.getResult()) {
if (document.exists()){
db.collection("main").document(userid).collection("journal").document(document.getId()).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Journal: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
} else {
Toast.makeText(SettingsActivity.this, "No data in JOURNAL", Toast.LENGTH_SHORT).show();
}
}
} else {
Toast.makeText(SettingsActivity.this, "Journal task: " + task.getException(), Toast.LENGTH_LONG).show();
}
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(SettingsActivity.this, "Error getting all the documents: " + e.getMessage(), Toast.LENGTH_LONG).show();
}
});
If you have multiple sub-collections in that particular document, to delete it you have to repeat that code multiple times for different sub-collections which will make the codes very long.
I hope this is helpful for someone out there who is trying to delete a user from Firestore. :) Use the extension already.
This query is trying to get all documents in the collection called "main":
db.collection("main").get()
Your security rules don't allow that. The rules only allow a user to read and write their own document in main.
It's not clear what exactly you're trying to delete, but if it's just the user document under main, you don't need a query at all. Just do this:
db.collection("main").document(userid).delete();
If you want to access all the documents under main collection you should change your Firebase rules to
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}
Although if you want to delete only single user data which you are doing in your code here :-
db.collection("main").get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
for (QueryDocumentSnapshot queryDocumentSnapshot : task.getResult()){
db.collection("main").document(userid).delete();
}
}
})
You shouldn't right the whole query you can simply delete the user document using
db.collection("main").document(userid).delete();
There is no API to delete an entire collection (or its contents) in one go.
From the Firestore documentation:
https://firebase.google.com/docs/firestore/manage-data/delete-data
"To delete an entire collection or subcollection in Cloud Firestore, retrieve all the documents within the collection or subcollection and delete them. If you have larger collections, you may want to delete the documents in smaller batches to avoid out-of-memory errors. Repeat the process until you've deleted the entire collection or subcollection."

Multiply reading of document references from Firestore using tasks

I have a Map of references which I read from Firestore. these refs lead me to documents that I'm willing to use their data to create an instance of my class 'Contact'.
In order to do that I've created a list of tasks which every task of it uses its ref to read from Firestore and retrieve the needed data.
Once it's all done I use Tasks.whenAll(tasks).addOnSuccessListener() willing to retrive my new array of Contacts.
On this method, 'contacts' is empty and 'data' is full of document references.
I expected Tasks.whenAll(tasks) to being called only when all this reading using the refs has completed, however it's being called immediately, therefore - nothing happens.
private void createContactArray(final ArrayList<Contact> contacts, final Map<String, DocumentReference> data) {
List<Task<DocumentSnapshot>> tasks = new ArrayList<>();
for (final Map.Entry<String, DocumentReference> entry : data.entrySet()) {
tasks.add(db.document(entry.getValue().getPath()).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
Map<String,String> contactDetails = (Map<String, String>) document.getData().get(entry.getKey());
Contact contact = createContact(contactDetails);
if(contact != null){ contacts.add(contact);}
} else {
Log.d(ACTION_FETCH_CONTACT_LIST,"There was ref problem with " + entry.getKey());
}
}else {
Log.d(ACTION_FETCH_CONTACT_LIST, "get failed with ", task.getException());
}
}
}));
}
Tasks.whenAll(tasks).addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
sendBroadcastActionContactList(contacts);
}
});
I would like Tasks.whenAll to be called once its all finished and not right away. I wish to have a proper explanation for the issue and a decent code that should do the job instead of mine.
I really appreciate your help!
You are using the APIs incorrectly. You should be collecting tasks returned by get() into an array, instead of immediately adding a callback to each one. Pass that list of tasks to Tasks.whenAll(). Then, in the callback for the task returned by Tasks.whenAll, you can examine each DocumentSnapshot results.

firestore read is asynchronous and I want synchronous behaviour

I want to store locally the data I am reading from the cloud.
To achieve this I am using a global variable(quizzes) to hold all the data.
For this, when I am building my Quiz objects, I need to make sure that before I am creating them, the relevant data has been already downloaded from the cloud. Since when reading data from firestore, it happens asynchronously.
I didn't enforced this (waiting for the read to finish) before -I just used onSuccess listeners, and I encountered synchronization problem because the reading tasks weren't finished before I created my Quiz objects with the data from the cloud.
I fixed this with a very primitive way of "busy waiting" until the read from the cloud is complete. I know this is very stupid, a very bad practice, and making the application to be super slow, and I am sure there is a better way to fix this.
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");
Task<QuerySnapshot> task = quizzesRefrence.get();
while(task.isComplete() == false){
System.out.println("busy wait");
}
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
}
I looked online in the documentation of firestore and firebase and didn't find anything that I could use. (tried for example to use the "wait" method) but that didn't help.
What else can I do to solve this synchronization problem?
I didn't understand if you tried this solution, but I think this is the better and the easier: add an onCompleteListener to the Task object returned from the get() method, the if the task is succesfull, you can do all your stuff, like this:
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference quizzesRefrence = db.collection("users").document(user_id).collection("quizzes");
quizzesRefrence.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccesful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
}
});
}
}
In this way, you'll do all you have to do (here the for loop) as soon as the data is downloaded
You can make your own callback. For this, make an interface
public interface FireStoreResults {
public void onResultGet();
}
now send this call back when you get results
public void readData(final FireStoreResults){
db.collection("users").document(user_id).collection("quizzes")
.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
results.onResultGet();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
results.onResultGet();
}
});
}
Now in your activity or fragment
new YourResultGetClass().readData(new FireStoreResults(){
#Override
public void onResultGet() {
new YourResultGetClass().getQuizzes(); //this is your list of quizzes
//do whatever you want with it
}
Hope this makes sense!

How to wait until firebase save all data?

I need some help with async tasks and Firebase. I need to save n itens on Firebase Realtime database, but I don't know how to deal with the callback.
Here's my code:
TagRepository tagRepository = new TagRepository();
for (Tag tag : databaseTags){
if (user.getTags().containsKey(tag.getId())){
Completable completable = tagRepository.saveUserOnTag(String.valueOf(tag.getId()), user.getUid());
}else{
tagRepository.removeUserOnTag(String.valueOf(tag.getId()), user.getUid());
}
}
public Completable saveUserOnTag(String idTag, String userUid) {
return io.reactivex.Completable.create(emitter->{
reference.child(idTag).child("users/").child(userUid).setValue(true).addOnCompleteListener(task -> emitter.onComplete());
});
}
If I use an callback on this method, the callback will be called n times, so I don't have any idea how to know when all of them are already saved so I can proceed.
I was trying something with Completable as you can see on the method, but I really don't know how to deal with it. There is any easy way to save all data at same time or to control all data that are being saved??
According to the official documentation, you can use simultaneous updates.
Using those paths, you can perform simultaneous updates to multiple locations in the JSON tree with a single call to updateChildren(). Simultaneous updates made this way are atomic: either all updates succeed or all updates fail.
You could try updating firebase data synchronously using Tasks.await
Change the saveUserOnTag method as follows and try
public Completable saveUserOnTag (String idTag, String userUid){
return io.reactivex.Completable.create(emitter -> {
Tasks.await(reference.child(idTag).child("users/").child(userUid).setValue(true));
emitter.onComplete();
});
Sorry I don't know Rx, but here is a way to insert all the data in one API call. The onComplete() method is the update complete callback.
Map<String, Object> values = new HashMap<>();
for (Tag tag : databaseTags){
if (user.getTags().containsKey(tag.getId())){
values.put(idTag + "/users/" + userUid, true);
}else{
tagRepository.removeUserOnTag(String.valueOf(tag.getId()), user.getUid());
}
}
reference.updateChildren(values, new DatabaseReference.CompletionListener() {
#Override
public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) {
// all data saved, do next action
}
});
Hope this helps :)

Categories

Resources