The structure of the table is:
chats
--> randomId
-->--> participants
-->-->--> 0: 'name1'
-->-->--> 1: 'name2'
-->--> chatItems
etc
What I am trying to do is query the chats table to find all the chats that hold a participant by a passed in username string.
Here is what I have so far:
subscribeChats(username: string) {
return this.af.database.list('chats', {
query: {
orderByChild: 'participants',
equalTo: username, // How to check if participants contain username
}
});
}
Your current data structure is great to look up the participants of a specific chat. It is however not a very good structure for looking up the inverse: the chats that a user participates in.
A few problems here:
you're storing a set as an array
you can only index on fixed paths
Set vs array
A chat can have multiple participants, so you modelled this as an array. But this actually is not the ideal data structure. Likely each participant can only be in the chat once. But by using an array, I could have:
participants: ["puf", "puf"]
That is clearly not what you have in mind, but the data structure allows it. You can try to secure this in code and security rules, but it would be easier if you start with a data structure that implicitly matches your model better.
My rule of thumb: if you find yourself writing array.contains(), you should be using a set.
A set is a structure where each child can be present at most once, so it naturally protects against duplicates. In Firebase you'd model a set as:
participants: {
"puf": true
}
The true here is really just a dummy value: the important thing is that we've moved the name to the key. Now if I'd try to join this chat again, it would be a noop:
participants: {
"puf": true
}
And when you'd join:
participants: {
"john": true,
"puf": true
}
This is the most direct representation of your requirement: a collection that can only contain each participant once.
You can only index known properties
With the above structure, you could query for chats that you are in with:
ref.child("chats").orderByChild("participants/john").equalTo(true)
The problem is that this requires you to define an index on `participants/john":
{
"rules": {
"chats": {
"$chatid": {
"participants": {
".indexOn": ["john", "puf"]
}
}
}
}
}
This will work and perform great. But now each time someone new joins the chat app, you'll need to add another index. That's clearly not a scaleable model. We'll need to change our data structure to allow the query you want.
Invert the index - pull categories up, flattening the tree
Second rule of thumb: model your data to reflect what you show in your app.
Since you are looking to show a list of chat rooms for a user, store the chat rooms for each user:
userChatrooms: {
john: {
chatRoom1: true,
chatRoom2: true
},
puf: {
chatRoom1: true,
chatRoom3: true
}
}
Now you can simply determine your list of chat rooms with:
ref.child("userChatrooms").child("john")
And then loop over the keys to get each room.
You'll like have two relevant lists in your app:
the list of chat rooms for a specific user
the list of participants in a specific chat room
In that case you'll also have both lists in the database.
chatroomUsers
chatroom1
user1: true
user2: true
chatroom2
user1: true
user3: true
userChatrooms
user1:
chatroom1: true
chatroom2: true
user2:
chatroom1: true
user2:
chatroom2: true
I've pulled both lists to the top-level of the tree, since Firebase recommends against nesting data.
Having both lists is completely normal in NoSQL solutions. In the example above we'd refer to userChatrooms as the inverted index of chatroomsUsers.
Cloud Firestore
This is one of the cases where Cloud Firestore has better support for this type of query. Its array-contains operator allows filter documents that have a certain value in an array, while arrayRemove allows you to treat an array as a set. For more on this, see Better Arrays in Cloud Firestore.
Related
So suppose I am building an app that lets users manage trips. When a trip is created , any number of users can be added in it. I want this data to be stored in a single place and then for each user be able to retrieve the trips that that person is included in. My data looks like this:
-trips
-<trip-id>
-title
-budget
-etc
-people
-<person-id>
-name
-uid
-<person-id>
-name
-uid
-<trip-id>
-
-
-
This trip will contain all the trips created by all the users. To show any person their trips, I want to retrieve only the lists that person exists in.
This is what I've tried to do including other similar approaches.
rootReference.child("trips").orderByChild("uid").equalTo(FirebaseAuth.instance.currentUser.uid).addValueEventListener(object:ValueEventListener){
override fun onDataChange(snapshot: DataSnapshot) {
//this should only return the trips that current user exists in.
}
}
I have checked the documentation for searching and filtering on firebase but there is nothing that show filtering based nested keys. One particular example is this. I understand it perfectly. If for example I try to filter my trips based on the main attributes like title, budget, it works, but not when I use an attribute of a nested child.
What other approach can I use to filter based to nested keys or should I structure the data differently? Any help is greatly appreciated.
Firebase Realtime Database queries operate on a flat list of child nodes directly under the path that you query.
So the value you order/filter on has to be at a fixex path under each immediate child node. Since that isn't the case for your uid, you can't query across all trips for the UID of all users of those trips.
You can query across one trip for a UID of a user (and then get back that user), or you can query across all trips for properties of the trip itself, such as its title or budget.
If you want to query across all users on all trips, consider keeping an additional list where you have the UID of the user as the key, and then all their trips under there:
"user_trips": {
"uid1": {
"tripid1": true,
"tripid2": true
},
"uid2": {
"tripid2": true,
"tripid3": true
}
}
Also see:
Firebase Query Double Nested
Firebase query if child of child contains a value
Many to Many relationship in Firebase
Let's say my current structure firebase realtime database:
{
chatRooms:{
chat1:{
participants:{
user1-id:true,
user2-id:true
}
participantsCount:2,
lastTimeUpdated: {TIMESTAMP},
chatRoomName:"A chatroom",
maxParticipantsCount:10
}
}
users:{
user1-id:{
name:user1-name,
email:user1-email
}
user2-id:{
name:user2-name,
email:user2-email
}
}
}
I need to display all chatrooms that contains a specific user along with all its information, and my solution now is:
#userId is currently authenticated, firebase user.
ref.child("chats").orderByChild("/participants/"+userId).equalTo(true)
which returns the list of chats containing $userId which equals to true, along with additional information to display to the user. Is there any possible way to change this query AND/OR database structure to support optimal indexing? If I wanted to add indices, it would be something like:
chatrooms:{
participants:{
indexOn:"user1-id","user2-id"
# number of ids grows as participants increase
# Not only that, I will have to add one by one manually.
}
}
How can I add indices and also at the same time do a query in Android that will display all chatrooms that a user belongs to along with their information?
AFAIK, I cannot "loop" over the keys of the child in Android
To loop over the children of a snapshot in Android do:
for (DataSnapshot childSnapshot: dataSnapshot.getChildren()) {
System.out.println(childSnapshot.getKey());
}
Relevant section in the docs: https://firebase.google.com/docs/database/android/lists-of-data#listen_for_value_events
If you're asking how to display the information about each chat room that a user is in, that will take a client-side join like this:
firebase.database().ref("userChatrooms").child(firebase.auth().currentUser.uid).once(function(roomsSnapshot) {
roomsSnapshot.forEach(function(roomKey) {
firebase.database().ref("chatrooms").child(roomKey.key).once(function(roomSnapshot) {
console.log(roomSnapshot.child("name").val());
})
})
})
This type of client-side join is not nearly as slow as most developers expect, since Firebase pipelines the nested requests.
How can i get All chats where Users contains a specific Uid
Query sItemQuery = mDatabase.getReference().child("chats").limitToLast(200);
FirebaseRecyclerOptions<ChatMetaData> options =
new FirebaseRecyclerOptions.Builder<ChatMetaData>()
.setQuery(sItemQuery, ChatMetaData.class)
.setLifecycleOwner(this)
.build();
You can't query across multiple levels like that. See Firebase Query Double Nested
So while your current data structure makes it easy to find the current users in a chat, it doesn't make it easy to find the current chats for a user.
For that you'll need to add an additional data structure. Something like:
user_chats: {
uid1: {
chatid1: true,
chatid2: true
}
uid2: {
chatid2: true,
chatid3: true
}
}
With this additional structure, you can easily find the chats for the current user by reading /user_chats/$uid.
Also see:
Firebase query if child of child contains a value
The structure of the table is:
chats
--> randomId
-->--> participants
-->-->--> 0: 'name1'
-->-->--> 1: 'name2'
-->--> chatItems
etc
What I am trying to do is query the chats table to find all the chats that hold a participant by a passed in username string.
Here is what I have so far:
subscribeChats(username: string) {
return this.af.database.list('chats', {
query: {
orderByChild: 'participants',
equalTo: username, // How to check if participants contain username
}
});
}
Your current data structure is great to look up the participants of a specific chat. It is however not a very good structure for looking up the inverse: the chats that a user participates in.
A few problems here:
you're storing a set as an array
you can only index on fixed paths
Set vs array
A chat can have multiple participants, so you modelled this as an array. But this actually is not the ideal data structure. Likely each participant can only be in the chat once. But by using an array, I could have:
participants: ["puf", "puf"]
That is clearly not what you have in mind, but the data structure allows it. You can try to secure this in code and security rules, but it would be easier if you start with a data structure that implicitly matches your model better.
My rule of thumb: if you find yourself writing array.contains(), you should be using a set.
A set is a structure where each child can be present at most once, so it naturally protects against duplicates. In Firebase you'd model a set as:
participants: {
"puf": true
}
The true here is really just a dummy value: the important thing is that we've moved the name to the key. Now if I'd try to join this chat again, it would be a noop:
participants: {
"puf": true
}
And when you'd join:
participants: {
"john": true,
"puf": true
}
This is the most direct representation of your requirement: a collection that can only contain each participant once.
You can only index known properties
With the above structure, you could query for chats that you are in with:
ref.child("chats").orderByChild("participants/john").equalTo(true)
The problem is that this requires you to define an index on `participants/john":
{
"rules": {
"chats": {
"$chatid": {
"participants": {
".indexOn": ["john", "puf"]
}
}
}
}
}
This will work and perform great. But now each time someone new joins the chat app, you'll need to add another index. That's clearly not a scaleable model. We'll need to change our data structure to allow the query you want.
Invert the index - pull categories up, flattening the tree
Second rule of thumb: model your data to reflect what you show in your app.
Since you are looking to show a list of chat rooms for a user, store the chat rooms for each user:
userChatrooms: {
john: {
chatRoom1: true,
chatRoom2: true
},
puf: {
chatRoom1: true,
chatRoom3: true
}
}
Now you can simply determine your list of chat rooms with:
ref.child("userChatrooms").child("john")
And then loop over the keys to get each room.
You'll like have two relevant lists in your app:
the list of chat rooms for a specific user
the list of participants in a specific chat room
In that case you'll also have both lists in the database.
chatroomUsers
chatroom1
user1: true
user2: true
chatroom2
user1: true
user3: true
userChatrooms
user1:
chatroom1: true
chatroom2: true
user2:
chatroom1: true
user2:
chatroom2: true
I've pulled both lists to the top-level of the tree, since Firebase recommends against nesting data.
Having both lists is completely normal in NoSQL solutions. In the example above we'd refer to userChatrooms as the inverted index of chatroomsUsers.
Cloud Firestore
This is one of the cases where Cloud Firestore has better support for this type of query. Its array-contains operator allows filter documents that have a certain value in an array, while arrayRemove allows you to treat an array as a set. For more on this, see Better Arrays in Cloud Firestore.
I am trying to understand how I can take a list of Ids, and then ask for Firebase to take each path and sort them by date and bring me back the top 20 all in one Firebase call. At the moment I am doing this by looping through the ids and grabbing each path - adding the items to a list then sorting them.
However this is inefficient especially as the list gets bigger.
My data looks something like this:
follows: {
UserId :{
projId1: true,
projId2: true
}
}
projects: {
projId1 :{
title: SomeText,
date: TimeStamp
}
}
A Firebase query runs at one specific location and it can only order on data that exists at fixed paths under the children of that location. There is no way to sort a Firebase query on data that exists at a different path.
So in your case, if you are loading data from /follows/$uid, there is not way to order the results on data from the /projects nodes.
That leaves you with two options:
sort the data on the client
at the sort properties to the follows children
Sorting data on the client is a valid option as long as your data set is not too large and you're not filtering too much.
But to allow sorting on the server, you'll have to duplicate the relevant properties under the node where you query:
follows: {
UserId :{
projId1: TimeStamp,
projId2: TimeStamp
}
}
projects: {
projId1 :{
title: SomeText,
date: TimeStamp
}
}
With this structure you can order then projects on timestamp with
Query query = database.child(user.getUid()).orderByValue();
query.addChildEventListener(...