How to query many to many relationship in Firebase for Android? - android

I'm trying to build a simple shopping list android app with a firebase database.
Quick context:
User registers with email and should be able to create a group to share a shopping list that belongs to the group (and all the members of the group). And then later on shopping list items are added to the shopping list.
I'm a bit stuck on how to handle the many-to-many relationship between the users and the groups.
I read this post: Many to Many relationship in Firebase
And based on that I created this data structure in Firebase:
What I don't really understand that how I will query this exactly. As we use value event listeners in Firebase, my guess was that I would need to attach the listener to the database reference and save the related data locally in the app to populate the screens.
For example, I would like a recycler view with all the groups that belong to the user, where the cards would display the name of the group, the creation date of the group, and the group members. Based on the above data structure, that's 4 separate value event listeners nested into each other, is this correct?
This is part of the code that I have written and this is only saving the userIDs for each group so it would require another nested query:
//get group ids - there is at least one as own_group is set up with the user details
Utils.userGroupReference.child(Utils.firebaseUserID).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot snapshot) {
for (DataSnapshot groupIDSnapshot : snapshot.getChildren()) {
Utils.myGroupIDs.add(groupIDSnapshot.getKey());
}
for (int i = 0; i < Utils.myGroupIDs.size(); i++) {
//get groups based on group ids
Utils.groupReference.child(Utils.myGroupIDs.get(i)).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot snapshot) {
Group group = (snapshot.getValue(Group.class));
GroupUser groupUser = new GroupUser(group);
Utils.myGroups.add(group);
Utils.groupUserReference.child(snapshot.getKey()).addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot groupUserSnapshot) {
ArrayList<String> userIDs = new ArrayList<>();
for (DataSnapshot userIDSnapshot: groupUserSnapshot.getChildren()) {
userIDs.add(userIDSnapshot.getKey());
}
Utils.myGroupUsers.add(new GroupUser(group, userIDs));
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
}
});
Utils.myGroupUsers.add(groupUser);
}
#Override
public void onCancelled(#NonNull DatabaseError error) {
}
});
I would appreciate it if someone could shed some light on this for me.
Another question is where do we normally call the value event listeners? This snippet is from the onCreate() method in the MainScreen which eventually would contain all the shopping lists. I was thinking to put it in the splash screen but my experience is that whenever a node is updated, the application will get thrown back to the screen that contains the value event listener, so I'm not sure which is a better option
Thank you very much!

I would typically use addListenerForSingleValueEvent listeners or getData calls for some of the inner data loading here, but it is correct that you'll need a separate call for each piece of data you load.
Since Firebase pipelines all these requests over a single connection, the data is loaded really quickly - so it's really just the code that gets a bit convoluted.
If that is something you'd like to prevent, you could consider duplicating some of the data from the User into each GroupUser and/or UserGroup entry where that user is present too. This will make the code that writes the data more complex, but simplifies the data loading.
This sort of trade-off is quite common when dealing with NoSQL databases, and is one of the reasons they can scale to so many users. To learn more about this, I recommend checking out NoSQL data modeling, Firebase for SQL developers, and Getting to know Cloud Firestore (the latter is for Cloud Firestore, but many of the lessons also apply here).

Related

firebase database how to know if parent exist?

I've the following scenario:
One user can have several photos, each photo can be reported by other users for violating the term of the app
So i do something like:
FirebaseDatabase.getInstance().getReference().child("reports").child(ownerId).child(g.getMediaId()).addListenerForSingleValueEvent(new ValueEventListener() {
...
...
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
if (!dataSnapshot.exists()) { //do stuff
}
}
The point is dataSnapshot.exists() returns if the whole branch exists... it may not exist because the mediaID is new (first report of that photo) or because the whole ownerId branch is new (first report of any photo of that user)
how can i know if the parent exists?
Just to summarize what I understand:
You have a JSON tree /reports/$ownerid/$mediaid.
You attach a listener to a specific /reports/ownerid/mediaid.
You want to check if the ownerid exists.
A listener gets all the data underneath the level where you attach it, but it gets no data from levels above where it is attached. So the only way to determine if /reports/ownerid exists, is to attach a listener on that level.
Note that this will then download all data under that /reports/ownerid, which may be more than you need. If that is the case, consider keeping a separate list of known_owners, where you track which owners you have data for.

Is using keepSynced() on Query instead of on DatabaseRef make any difference?

I am currently getting a single dataSnapshot from Firebase like so:
public Task<DataSnapshot> getLatestMessage(#NonNull String roomId) {
final TaskCompletionSource<DataSnapshot> source = new TaskCompletionSource<>();
DatabaseReference dbRef = mDatabase.getReference(NODE_MESSAGES).child(roomId);
dbRef.keepSynced(true);
ValueEventListener listener = new ValueEventListener() {
#Override
public void onDataChange(#NonNull DataSnapshot dataSnapshot) {
source.setResult(dataSnapshot);
}
#Override
public void onCancelled(#NonNull DatabaseError databaseError) {
source.setException(databaseError.toException());
}
};
Query query = dbRef.orderByKey().limitToLast(1);
query.addListenerForSingleValueEvent(listener);
return source.getTask();
}
Notice that I called keepSynced() on dbRef object already.
Here's the sample data structure:
/root
/messages
/$roomId
/$messageId
/content
/timestamp
/etc...
I am able to get the most recent single snapshot data as expected, but I was wondering, does it make any difference if I move the keepSynced() call in the Query object instead of the DatabaseReference? i.e.
// dbRef.keepSynced(true); >> REMOVE THIS <<
ValueEventListener listener = new ValueEventListener() {...};
Query query = dbRef.orderByKey().limitToLast(1);
query.keepSynced(true); // >> ADD THIS <<
query.addListenerForSingleValueEvent(listener);
We're currently averaging 50% load (per day) on Firebase right now and with the steady inflow of users, I was wondering if it could improve anything in the app somehow, specially with the load. I even tried something as silly as this:
dbRef.keepSynced(true);
ValueEventListener listener = new ValueEventListener() {...};
Query query = dbRef.orderByKey().limitToLast(1);
query.addListenerForSingleValueEvent(listener);
dbRef.keepSynced(false);
-- enabling keepSynced() at the start to make sure that the reference is pointing to the most recent version, then disabling it after querying and adding the listener. Unfortunately, this doesn't provide the fresh data, not like when keeping it enabled.
I've already gone through the Optimization DB Performance documentation and believe that I followed the suggested practices as needed.
Putting the keepSynced() call on a limitToLast() query will not make any difference on the load on the database server. The server needs to load the exact same data and monitor it, it just only returns the last item to the client.
I recommend using keepSynced sparingly in your app. Each call to keepSynced keeps an empty listener to the reference/query you attach it to. That means that each client has an active listener to each chat room you call keepSynced on, even when the user is not looking at that room. While that may be precisely the right call for the use-cases of your app, it will limit the scalability of your app.
If you're worried about reaching peak load, you might want to consider looking into how to shard your data over multiple databases. Chat apps are typically relatively easy to shard, since each chat room is already isolated.

Flattening data in Firebase to reflect model in Android app

I'm trying out Firebase as my backend for a prototype app im creating.
As a mockup, im creating a fake game.
The data is pretty simple:
There are 3 lists of 'levels' common to all users, organized by difficulty ( easy, medium hard ). ( These are in a Fragment inside a ViewPager
Each level has mini-games inside of them than the users complete.
On the Android app, the user sees that list but also sees a counter of how many mini-games of that level he/she has completed.
If the user clicks on a level, he/she sees the list of mini-games and sees which of those are completed.
Im currently structuring my data as follows:
levels:{
easy:{
easy-level-1-id:{
total-mini-games:10,
easy-level-1-id:String
}
},
medium:{....},
hard:{.....}
}
user-progress:{
user-id:{
levels:{
easy-level-1-id:{
user-completed:int
}
},
mini-games:{
mini-game-1:true
mini-game-2:true
}
}
}
I have to access many places in order to check if a game has been completed, or how many games have been completed per level.
All of this is in order to avoid nesting data, as the docs recommend.
Is it better to do it like this or is it better to store every available level under every user id in order to do less calls, but having much more repeated data ?
I know about the FirebaseRecyclerViewAdapter class provided by the Firebase team, but because it takes a DatabaseReference object as an argument for knowing where the data, it doesn't really apply here, because i fetch data from different structures when building the model for the lists.
mRef1.child(some_child).addValueEventListener(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot dataSnapshot){
final Model model = dataSnapshot.getValue(Model.class);
mRef2.child(some_other_child).addValueEventListener(new ValueEventListener(){
#Override
public void onDataChange(DataSnapshot dataSnapshot){
//add more data into the model class
.....
}
#Override
public void onCancelled(DatabaseError databaseError){}
});
#Override
public void onCancelled(DatabaseError databaseError){}
});
Any pointers as to how to work with more structured data ?
FirebaseUI includes support for working with indexed data. It can load linked data from an index, such as in your mini-games node.
In your example that could work as:
keyRef = ref.child("user-progress").child(currentUser.getUid()).child("mini-games");
dataRef = ref.child("mini-games");
adapter = new FirebaseIndexRecyclerAdapter<Chat, MiniGameHolder>(
MiniGame.class,
android.R.layout.two_line_list_item,
MiniGameHolder.class,
keyRef, // The Firebase location containing the list of keys to be found in dataRef.
dataRef) //The Firebase location to watch for data changes. Each key key found at keyRef's location represents a list item in the RecyclerView.

How firebase listener actually work?

I'm wondering on Android, how the underlying actual mechanism work when you add an listener to the database. Is it just more frequent pulling or something else special?
Update:
To make it clearer, I understand what a listener is, but I meant how does the 'listening' scheme work, how a client (Android) knows the data on the server changed. Is it just a periodical pulling? (and Firebase engineers already do the hard work to cover that and make it easy for us).
Looks like firebase is not open-source.
// Attach an listener to read the data at our posts reference
ref.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
System.out.println(snapshot.getValue());
}
#Override
public void onCancelled(FirebaseError firebaseError) {
System.out.println("The read failed: " + firebaseError.getMessage());
}
});
disclaimer: this is a simplified description of how things work at the time of writing. Things may have changed by the time your read it.
When your app connects to the Firebase Database, it opens a web socket connection from the device to a Firebase server. This connection stays open for the lifetime of your app or until you call goOffline().
When you attach a listener, the client sends the location (and potential query parameters) to the server. The server adds that listener to a list of all listeners of all connected clients. It then also sends back the initial data for that listeners.
Whenever a write operation is committed to the database, the server scans the listeners. For each relevant listener, the server sends an update to the client over the open web socket.
It happens Asynchronously
Adding listeners to a node reference will fetch any changes made to the node reference asynchronously
void onDataChange(DataSnapshot snapshot)
This method will be called with a snapshot of the data at this location. It will also be called each time that data changes.
void onCancelled(FirebaseError error)
This method will be triggered in the event that this listener either failed at the server, or is removed as a result of the security and Firebase rules. For more information on securing your data, see: Security Quickstart
Example
ref.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
System.out.println(snapshot.getValue());
Users users = snapshot.getValue(Users.class) //This is your POJO class
String name = users.getName(); //Other getter methods to fetch data from firbase
}
#Override
public void onCancelled(FirebaseError firebaseError) {
System.out.println("The read failed: " + firebaseError.getMessage());
}
});
It's explained in the guide Firebase - Retrieve Data on Android
This method is triggered once when the listener is attached and again every time the data, including children, changes.

Firebase on Android - how to make event handler for data retrieval fire when I need it to?

I am new to Firebase and need some help with a query to retrieve data from a table. I am currently able to access and retrieve the data that I need from firebase, however, the timing is the problem I am having an issue with.
From everything I've seen, the firebase database requires me to add event listeners to the Query or DatabaseReference objects. I am trying to download the contents of a node called "questions" before a method to display the question contents is called, however, I cannot control the timing of the firing of the event which downloads the data, and as a result my display method is always called before the firebase event fires.
How can I execute a query when I want, and be sure it will be completed before a certain section of my code executes? I am used to traditional RDBs where you execute a query and get its results and then move forward with your logic. The need to use an event handler with firebase is what I am having a hard time with. I have even tried moving the definition of the firebase reference object and the event handler into onCreate() and moved the code that calls my display method into onStart() without any success - same problem. The data I am trying to get does not change so I only need to download it once at the beginning to have available for the display method.
Here is an image of my "questions" node which is a child of the root.
image of the child "questions" node on my firebase DB
Here is my code:
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get Firebase DB reference
firebase = FirebaseDatabase.getInstance();
fdbRef = firebase.getReference("questions");
// [START Question_event_listener]
fdbRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get Questions object and use the values to update the UI
objQuestions = dataSnapshot.getValue();
Log.w("Firebase:", "In Firebase ValueEventListener");
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting Questions failed, log a message
Log.w("Firebase Error:", "onCancelled:", databaseError.toException());
Toast.makeText(ReviewActivity.this, "Failed to load question!", Toast.LENGTH_SHORT).show();
}
});
//. . . remaining onCreate logic removed for simplicity
} //end of onCreate
#Override
public void onStart() {
super.onStart();
// I moved this logic from onCreate to onStart but did not help...
// Firebase retrieve must execute before I call any of these
if (list_type == MainActivity.LIST_UNREVIEWED_DOCS)
displayNewReviewForm();
else if (list_type == MainActivity.LIST_REVIEWS)
displayCompletedReview();
else // (list_type == MainActivity.LIST_DRAFTS)
displayDraftReview();
}
Other alternatives if I can't get this resolved may be to move this retrieve logic to the prior Activity in my sequence and pass the retrieved data as an extra to this activity - but that seems really silly to have to do such a thing. I would think I should be able to get data from a DB when I need it... not when it feels like giving it to me.
I appreciate any help getting me past this issue.
Your code is downloading the snapshot data containing all the data at the first go only, and with Firebase, you cannot download data timely, you can only do it through different references.
What I would suggest you to do is, to have a DatabaseReference of q01, q02 respectively and then call data as in when required.
If your Keys "q01", "q02" are static, which they are looking at the scenario. I would suggest you to have their DatabaseReferences:
question_one = firebase.getReference("q01");
question_two = firebase.getReference("q02");
question_three = firebase.getReference("q03");
//Once you have the reference, you can call their ValueListeners respectively
question_one.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
// Get Questions object and use the values to update the UI
objQuestions = dataSnapshot.getValue();
Log.w("Firebase:", "In Firebase ValueEventListener");
}
#Override
public void onCancelled(DatabaseError databaseError) {
// Getting Questions failed, log a message
Log.w("Firebase Error:", "onCancelled:", databaseError.toException());
Toast.makeText(ReviewActivity.this, "Failed to load question!", Toast.LENGTH_SHORT).show();
}
});
After looking at this a bit more, I came up with 2 possible solutions to the problem I had.
The first one I sort of mentioned already in my original question post, however it's not ideal in my opinion. It basically involves relocating the firebase retrieve logic to the prior Android Activity and passing the retrieved data to the Activity I need it in as an Extra. In my case the data is a HashMap so I would need to use the serialize versions of the methods to pass the serialized content to the desired Activity.
The best solution, is much simpler. I basically relocated the logic that I had in the onStart() function (which is calling my custom display methods) and moved it inside of the Firebase Event Listener's onDataChange() method, right after the call to dataSnapshot.getValue(). This ensures that I get the data before I call my display methods. This seems to be working well now.

Categories

Resources