search for sub-document value in firestore - java

I am using firestore database that has the following structure:
I have collection named NBDB, inside there are documents with the uid of each user. For example I have 2 users so I have 2 documents called: ZxK2BR..., xy9BHY....
In each user document there is another collection called MyBooks and there are documents for all the books the user search.
Here are pictures from my database:
I want to make like home page screen that will disaply 20 random images from all of the images that users have in the app database.
From my understanding I need to get inside each user document and search for all the my books it has and then to move for the next user.
My finally goal is to read the BookID value of all of the users and to choose 20 random
I used the following to obtain all BookID from specific user:
FirebaseFirestore db = FirebaseFirestore.getInstance();
CollectionReference MyDB = db.collection( "NBDB" ).document(auth.getUid()).collection( "MyBooks" );
MyDB.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.getString( "BookID" ));
}
Log.d(TAG, list.toString());
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
Thank you

I think what you're looking to do is pull in 20 different images, at random, from any user's sub-collection. That said, if you don't choose to use another structure and wish to have all of the data nested, you can opt for a collectionGroup query.
The bit on "random" is a bit tricky, and you may want to use some hacky ways to make it work. But starting with getting access to the various user's nested sub collections:
All of this data, regardless of user, lives in collections called MyBooks so you can run a collectionGroup query on MyBooks.
The way I would do it, with the random-image hacky bit would be, in partial pseudocode code to give you an idea (I confirmed collectionGroups in the Java SDKs):
usedImages = []
for numbers 0 to 19 {
let num = false
while (!num) {
let tempNum = Math.floor(Math.random() * 10);
if (!( usedImages.includes(tempNum)){
num = tempNum
}
}
var allBooks = db.collectionGroup('MyBooks').where('imageKey', '==', num);
allBooks.get().then(function (querySnapshot) {
querySnapshot.forEach(function (doc) {
console.log(doc.id, ' => ', doc.data());
});
});
}
The idea with the code is to generate 20 random and unique numbers num (probably better ways to do it than what I hacked together above), and then take that number and look for all entries in MyBooks where that imageKey exists. This is the hacky way around the random image code, where you would need to add a globally unique ID to each of them.
Collection Group doesn't specify a path of parent collections, so it allows you to look into all collections at once.
In the above pseudocode, you're running 20 collection groups queries.
You can find more on Collection Group queries here: https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query

Related

is there any way to fetch records from firebase realtime database whose attribute has a value in my predefined list?

Basically what I am trying to do is I have a database with the name users having an attribute username. I have some usernames in one list and I want to show details of these users only whose username is present in the list. How can I write a query to fetch details of those users only whose username is found in this list? And note that there is no lexicographical ordering so i can't use startAt() and endAt() functions as well.
code snippet:
=> myList contains usernames. This code doesn't yield accurate results.
Any help would be really appreciated! Thank you!
FirebaseRecyclerOptions<MainModel> options =
new FirebaseRecyclerOptions.Builder<MainModel>()
.setQuery(FirebaseDatabase.getInstance().getReference().child("users").orderByChild("username")
.startAt(myList.get(0)).endAt(myList.get(myList.size()-1)),MainModel.class).build();
As already mentioned in the comment, the Firebase-UI library doesn't help in your case, because it doesn't allow you to pass multiple queries to the FirebaseRecyclerOptions object. So you need to perform a separate query and use the combined result.
When you are calling .get() on a Firebase Realtime Database query object, you are getting back a Task object. So the key to solving this problem is to use whenAllSuccess(Collection> tasks). In your case, it should look like this:
DatabaseReference db = FirebaseDatabase.getInstance().getReference();
DatabaseReference usersRef = db.child("users");
Query query = usersRef.orderByChild("username");
List<Task<DataSnapshot>> tasks = new ArrayList<>();
for (String username : myList) {
tasks.add(query.equalTo(username).get());
}
Tasks.whenAllSuccess(tasks).addOnSuccessListener(new OnSuccessListener<List<Object>>() {
#Override
public void onSuccess(List<Object> list) {
//Do what you need to do with your list.
for (Object object : list) {
MainModel mm = ((DataSnapshot) object).getValue(MainModel.class);
if(mm != null) {
Log.d("TAG", mm.getUsername());
}
}
}
});
Assuming that you have in your MainModel class a getter called getUsername(), the result in your logcat will be all the usernames of all returned children.

How can I write Arraylist data to Firebase Database in Java?

I have an android app with firebase as the backend. I have an activity where the use populates a list for a recycler view which they want to save to the database.
Below is a screenshot of the Firebase Realtime Database structure of my database:
The values of children in the Values node is what I'm using to create the id for new data that is added.
I have an arraylist of sales objects populated by the user which is to be saved in a new Sales node and would like to use the value in Values/Sales to populate the ids for each item in the arraylist.
Below is my code for saving that data. However in the database, only the last item in the arraylist is saved.
ArrayList<Inventory> salesArrayList = new ArrayList<>();
------------------- Code for populating data into the arraylist ----------------------
for (int i = 0; i < salesArrayList.size(); i++){
int finalI = i;
databaseReference.child("Values").child("Sales").addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot snapshot) {
String newCount = String.valueOf(Integer.parseInt(String.valueOf(snapshot.getValue()))+1); // Gets the original value saved and adds 1 to it to be used as the id in the 'Sales' node
DatabaseReference countReference = databaseReference.child("Sales").child(newCount);
countReference.child("date").setValue(currentDate);
countReference.child("name").setValue(salesArrayList.get(finalI).getName());
countReference.child("quantity").setValue(salesArrayList.get(finalI).getQuantity());
countReference.child("unit_price").setValue(salesArrayList.get(finalI).getValue()/salesArrayList.get(finalI).getQuantity());
countReference.child("value").setValue(salesArrayList.get(finalI).getValue());
databaseReference.child("Values").child("Sales").setValue(Integer.parseInt(newCount)); // Updates the number of sales
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
}
});
}
If there are 7 items in the arraylist, I'd like a way to have all them saved in the Sales similar to the Purchases node rather than only the last item on the arraylist.
This doesn't work (and I'm surprised it even compiles):
databaseReference.child("Values").child("Sales")(Integer.parseInt(newCount)); // Updates the number of sales
To write back the incremented number of sales:
snapshot.getReference().setValue(snapshot.getValue(Long.class)+1);
Note that in general using such sequential numeric keys in Firebase is an anti-pattern. I highly recommend using Firebase's native push() keys, which are also always incrementing but provide much stronger guarantees outside of that. To read more about that, check out: Best Practices: Arrays in Firebase.
I can't comment, but I can answer, but I don't know if my answer will be correct.
If you are only updating the last item on the array it would seem to me that you are at each iteration overwriting the same object and finally saving only that object to the database.
Is there an append method? Or does the documentation say anything about this?
This for me seems the most probable cause.
Maybe you each time need a new instance of the 'databaseReference'-object.

Is there any way to make my app wait for data to be retrieved from Firebase before continuing the code?

My app has to display a list of names on ListView. Those names are stored within Cloud Firestore in the following manner:
Collection: users - Documents: Organized by user UID - Field: name (I must note that there are other fields for each user too, however i need to retrieve the name field specifically)
To accomplish this, I have a first list that retrieves all documents or user UIDs. That first list is then used within a for loop to retrieve the name of each user in the users collection.
However, due to Firebase retrieving data asynchronously, some names are usually missing and they end up being displayed in a disorganized manner (not consistent with the order in which uids were passed from the first list).
If anyone could give me any insight on how to make Firebase wait for data to be retrieved before continuing with the for loop it would be greatly appreciated!
Below is some of my code to give you a better idea of what I am doing.
This first part of the code, which successfully retrieves all documents (uids) and puts them on a list
subTopicsDatabase.collection("schoolTopics").document(docKey).get().addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()){
DocumentSnapshot document = task.getResult();
if (document.exists()) {
List<String> list = new ArrayList<>();
Map<String, Object> map = document.getData();
if (map != null) {
for (Map.Entry<String, Object> entry : map.entrySet()) {
list.add(entry.getValue().toString());
}
}
}});
The second part of the code, which doesnt work due to Firebase's asynchronous behavior.
for (int i = 0; i<list.size(); i++) {
String uid = list.get(i);
Toast.makeText(TutorsListActivity.this, uid, Toast.LENGTH_LONG).show();
subTopicsDatabase.collection("users").document(uid).get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists()) {
String stName = documentSnapshot.getString("name");
ArrayAdapter<String> adapter = new ArrayAdapter<>(TutorsListActivity.this, R.layout.item_subtopic, testList);
adapter.notifyDataSetChanged();
sListView2.setAdapter(adapter);
}
}
});
}
You need to store the elements and in last of the for loop, you have to show the names list.
As you said you are getting a list of All UID's now you want their names on a list. I had updated your code to work.
// Create a Hashmap Object which has Key as UID and Name as Key
HashMap<String,String> hashMap = new HashMap<>();
for (int i = 0; i<list.size(); i++) {
final String uid = list.get(i);
Toast.makeText(TutorsListActivity.this, uid, Toast.LENGTH_LONG).show();
subTopicsDatabase.collection("users").document(uid).get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists()) {
//Store Your UID and Name in Hashmap
String stName = documentSnapshot.getString("name");
hashMap.put(uid,stName);
}
//Check if it is last index of array then show the names list
if(i==list.size()-1){
showListInAdapter(hashMap);
}
}
});
}
private void showListInAdapter(HashMap<String,String> hashMap) {
//now convert your hashmap into a list of name and get Your Names List and show in Adapter
ArrayList<String> listOfNames = new ArrayList<>(hashMap.keySet());
//Set list to Adapter
ArrayAdapter<String> adapter = new ArrayAdapter<>(TutorsListActivity.this, R.layout.item_subtopic, listOfNames);
sListView2.setAdapter(adapter);
adapter.notifyDataSetChanged();
}
Your assertion that it doesn't work because of Firebase's asynchronous behavior is incorrect. The reason that your view is not displaying the way you want it to, is because you are updating your adapter every single time you receive a document back from Firebase.
In psuedocode, this is what should happen:
// Create function with completion block - i.e. fetchTopicNames
//
// Create array to hold fetched String values - i.e. topicNames
// For loop to request each document
// add String value to `topicNames`
// if current iteration is last iteration, finish forLoop and return topicNames
//
In another method, call your newly created method, update your adapter with your full list of topicNames. You can also then perform other operations on your Array like filtering and sorting. There is probably a more efficient way as well, I'm just giving you the most basic way to accomplish your task.
you can simulate fetching user synchronous by making recursion (function which call it self until index becomes bigger then size of list of uids).
So first thing you want to define adapter and List of strings (which represent user names). When you do that, you can call recursion, which will populate your List and notifyDataSetChanged. Here is the example
// Define empty list of user names, which you will populate later with recursion
List<String> userNames = new ArrayList<String>();
// Connect adapter with empty list
ArrayAdapter<String> adapter = new ArrayAdapter<>(TutorsListActivity.this, R.layout.item_subtopic, userNames);
// Set adapter to ListView
sListView2.setAdapter(adapter);
// Call recursion with list of uids and starting index of 0
getUserSync(list, 0);
private void getUserSync(List<String> list, int i) {
if (i < 0 || i > list.length - 1) {
// If index i is out of bounds for list, we break the recursion
return;
}
String uid = list.get(i);
Toast.makeText(TutorsListActivity.this, uid, Toast.LENGTH_LONG).show();
subTopicsDatabase.collection("users").document(uid).get().addOnSuccessListener(new OnSuccessListener<DocumentSnapshot>() {
#Override
public void onSuccess(DocumentSnapshot documentSnapshot) {
// When we load document, we fetch name and add it to the list which is connected to adapter
// After that, we call adapter.notifyDataSetChanged which will update ui
// When all that is done, we call getUserSync, to fetch user name for next uid
if (documentSnapshot.exists()) {
String stName = documentSnapshot.getString("name");
if (stName != null) {
userNames.add(stName);
adapter.notifyDataSetChanged();
}
}
getUserSync(list, i++);
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
// If enything goes wrong, we break the recursion
return;
}
});
}
If you have any troubles, feel free to comment..
I recommend that you follow the Guide to App Architecture and use a LiveData observer to keep the ListView updated. You can follow this tutorial and insert your Firebase access in the Repository class.
Changing your code to fit the MVVM pattern may require a bit of work but it will also make your app run better and simplify some development later.

Firebase Cloud Firestore : Invalid collection reference. Collection references must have an odd number of segments

I have the following code and getting an error :
Invalid collection reference. Collection references must have an odd number of segments
And the code :
private void setAdapter() {
FirebaseFirestore db = FirebaseFirestore.getInstance();
db.collection("app/users/" + uid + "/notifications").get().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
for (DocumentSnapshot document : task.getResult()) {
Log.d("FragmentNotifications", document.getId() + " => " + document.getData());
}
} else {
Log.w("FragmentNotifications", "Error getting notifications.", task.getException());
}
});
}
Then you need replace it:
db.collection("app/users/" + uid + "/notifications")...
with it:
db.collection("app").document("users").collection(uid).document("notifications")
You're welcome ;)
Hierarchical data structures and subcollections are described in the documentation. A collection contains documents and a document may contain a subcollection. The structure is always an alternating pattern of collections and documents. The documentation contains this description of an example:
Notice the alternating pattern of collections and documents. Your
collections and documents must always follow this pattern. You cannot
reference a collection in a collection or a document in a document.
Thus, a valid path to a collection will always have an odd number of segments; a valid path to a document, an even number. Since your code is trying to query a collection, the path length of four is invalid.
You are missing collection reference.
i.e db.collection(** This is getting null **).
I've encountered this issue when I provided a wrong entity_Id.
Instead of dojo/default/datasets/fe67ec58-6208-4234-a4ee-98c5dce4665f,
I've provided fe67ec58-6208-4234-a4ee-98c5dce4665fand now is working fine.
I've encountered this issue when I provided an entity_Id that contains the "/" character ( my value was N/A ) when i was trying to read documentReference (DocumentReference docRef2 = fireStoreDb.Collection("Ass").Document(ass.Tel.ToString())
.Collection("vehicules").Document(ve.Immatriculation);).
Here, the value ve.Immatriculation equals to N/A, and that was the problem.

How to use Firebase query equalTo(value, key)?

As a newbie in firebase I tried to mimic a kind of "where clause" request to retrieve the user's wallet in this simple use case:
User
48bde8f8-3b66-40bc-b988-566ccc77335c
email: "toto#acme.com"
username: "userTest1"
UserWallet
F4PvtvNT2Z
coins: 26
someList
elemet1
elemet2
user: "48bde8f8-3b66-40bc-b988-566ccc77335c"
First I tried to code my request like this:
Firebase root = new Firebase("https://myApp.firebaseio.com/");
Firebase ref = root.child("UserWallet");
Query query = ref.equalTo("48bde8f8-3b66-40bc-b988-566ccc77335c", "user");
The result was null, So I wrote this query:
Query query = ref.orderByChild("user").equalTo("48bde8f8-3b66-40bc-b988-566ccc77335c", "user");
Result was null again. The only way to retrieve the wallet was with this query:
Query query = ref.orderByChild("user").equalTo("48bde8f8-3b66-40bc-b988-566ccc77335c");
So Should I have to always use en "orderByChild()" query before to use "equalTo()"?
And so, what is the purpose of the query "equalTo(String value, String key)" compare to "equalTo(String value) since only the second one works correctly?
There are some edge cases that don't need an orderBy...(), but in general you'll need an orderBy...() before a filtering operation (equalTo(), startAt(), endAt()).
I highly recommend that you first read the Firebase programming guide for Android (95% of it applies to regular Java too). A few hours spent in that guide, will save dozens of questions here. For example: this is the section on queries.
After reading that, you might also want to read this guide on NoSQL Data Modeling. It covers many common patterns in NoSQL data modeling and will help you realize early on that trying to map SQL queries to a NoSQL database is a logical idea, but seldom a good one.
My initial (without any idea on your use-cases, except for "I need to be able to find the wallets for a user") model:
UserWallet
"48bde8f8-3b66-40bc-b988-566ccc77335c"
"F4PvtvNT2Z"
coins: 26
someList
element1
element2
In the above model, I've inverted the Wallet and User under UserWallet, so that looking up the wallet(s) for a user becomes easier.
ref.child('UserWallet').child(auth.uid).addValueEventListener(...
Note that there is no query involved here, so loading will be equally fast no matter how many users you have in your database.
Or alternatively:
User
"48bde8f8-3b66-40bc-b988-566ccc77335c"
email: "toto#acme.com"
username: "userTest1"
Wallet
"F4PvtvNT2Z"
coins: 26
someList
element1
element2
UserWallet
"48bde8f8-3b66-40bc-b988-566ccc77335c"
"F4PvtvNT2Z"
Now we've complete flattened the structure. To determine the wallets of a user, you go to UserWaller/$uid and then load each wallet from Wallets/$walletid. It may be a bit more code, but it'll be extremely efficient (since there are no queries involved).
You can use nested Query for this.! if you have multiple random id you can easily compare them.!
DatabaseReference reference = FirebaseDatabase.getInstance().getReference();
Query query = reference.child("user");
query.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
// dataSnapshot is the "issue" node with all children with id 0
for (DataSnapshot issue : dataSnapshot.getChildren()) {
// do something with the individual "issues"
Query query = reference.child("user").child(dataSnapshot.getKey().equals(YourData)));
query.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
if (dataSnapshot.exists()) {
}
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});

Categories