What is the most efficient way to get 3 documents (marked red) from firebase collection? One more obstacle for me is, that user document has auto-generated Id. The code below works but I think is not efficient way to do it. If 'times' collection grows, retrieving whole collection while want only one or two documents doesn't make sense.
If there is no way to improve it, any suggestions of modifying my firestore structure to achieve the goal?
FirebaseFirestore db = FirebaseFirestore.getInstance();
db.collection("users")
.whereEqualTo("userName", "tolek")
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
// task - retrieves entire user document with all fields and collections, what I don't want - want only document Id
if (!task.getResult().getDocuments().isEmpty()) {
db.collection("users")
.document(task.getResult().getDocuments().get(0).getId())
.collection("times")
.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (!task.getResult().getDocuments().isEmpty()) {
for (DocumentSnapshot snapshot : task.getResult().getDocuments()) {
Log.d("TAG", "Documents : " + snapshot.getId());
//here also I receive whole "times" collection but want only 3 documents
}
}
}
});
}
}
});
To simplify the loading of the 3 times documents, you can use an in query:
CollectionReference timesRef = db.collection("users")
.document(task.getResult().getDocuments().get(0).getId())
.collection("times");
timesRef.whereIn(FieldPath.documentId(), Arrays.asList("20210225", "20210226", "20210227"));
This works for up to 10 values. If you have more than 10 IDs, you'll need to fire more than one query.
Related
I have two collections in my firestore database, the first is list of all the documents (BlockList), and the second for the users. when the user bookmark post on Recyclerview on the app, send the id of the document and timestamp to sub-collection (Favorites) under Users Collection.
i retrieve the list of documents based on this ids from the main collection (BlockList), but i want to arrange them according to timestamp for each user, so i've tried to order them by timestamp before adding them to Arraylist and Log the list, i've got the correct result. but the recyclerview still retrieved them ascending order by document ID !
firebaseFirestore.collection("Users")
.document(userId).collection("Favorites").orderBy("timestamp", Query.Direction.DESCENDING).get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<String> list = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
list.add(document.getId());
}
Log.d(TAG, list.toString());
if (list.size() != 0){
blockReffav = firebaseFirestore.collection("BlockList")
.whereIn(FieldPath.documentId(), list);
blockReffav.get().addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if(task.isSuccessful()){
onFirestoreTaskComplete.blockListDataAdded(task.getResult().toObjects(BlockListModel.class));
} else {
onFirestoreTaskComplete.onError(task.getException());
}
}
});
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Given how you currently retrieve the documents:
blockReffav = firebaseFirestore.collection("BlockList")
.whereIn(FieldPath.documentId(), list);
There is no way to specify the order of the IDs in the list in this query, so you will get them back in (the default) order of document IDs.
I see two options:
Request the documents one by one, so that you get them in the correct order.
Re-order the documents in your application code to match the order in the list.
I'd recommend the latter, as it's likely to be both simpler and more reliable.
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."
I am trying to download some Quiz objects from my database.
The following function is called from onCreate of a certain activity.
private void downloadQuizzesFromCloud(){
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
String user_quizzes_path = "users/".concat(user_id).concat("/quizzes");
Query userQuizzes = db.collection(user_quizzes_path);
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
quizzes.clear();
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
Toast.makeText(QuizzesActivity.this,"downloaded to list ".concat(String.valueOf(quizzes.size()).concat(" quizzes")), Toast.LENGTH_SHORT).show();
//TODO put in recycle adapter
} else { }
}
});
}
(user_quizzes_path contains the correct path to a collection of Quiz objects stored on the cloud)
I debugged this functions and found out that after the command:
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>()
The function finishes execution, that is the onComplete cases aren't checked and executed and all this code is just skipped.
I tried to find this on the documentation of firebase but didn't find anything.
Why is this happening and how can I fix this?
Would appreciate some help here, thanks!
The onComplete is called when the read operation has completed from the Firestore servers. If it's not getting called, I can see two possible reasons:
You're not connected to the server. Unless you've read the data before (and it's in the local database that the Firestore client maintains), this means the read never completes locally.
You're not thinking asynchronously. Note that data is read from the server asynchronously, and there may be some time between when you call get() and when onComplete fires. To test if this is the case, put a breakpoint on if (task.isSuccessful()) { and run the app in the debugger. The breakpoint will hit when the data is read from the server.
Use a callback interface. Just like this below.
private void downloadQuizzesFromCloud(Consumer listener) {
String user_id = FirebaseAuth.getInstance().getCurrentUser().getUid();
final FirebaseFirestore db = FirebaseFirestore.getInstance();
String user_quizzes_path = "users/".concat(user_id).concat("/quizzes");
Query userQuizzes = db.collection(user_quizzes_path);
userQuizzes.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
List<Quiz> quizzes = new ArrayList<>();
for (QueryDocumentSnapshot document : task.getResult()) {
Quiz quizDownloaded = getQuizFromCloud(document.getId());
quizzes.add(quizDownloaded);
}
listener.onGet(quizzes);
Toast.makeText(QuizzesActivity.this,"downloaded to list ".concat(String.valueOf(quizzes.size()).concat(" quizzes")), Toast.LENGTH_SHORT).show();
//TODO put in recycle adapter
} else { }
}
});
}
interface Consumer {
void onGet(List<Quiz> quizzes);
}
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!
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