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
        }
    });
 }

1 Answers 11

up vote 29 down vote accepted

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 fixed paths

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 require than you 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.

upvote
  flag
This is an awesome explanation. I have my data stored like this, which allows me to grab a chatroom from a user's list, or grab a user from a chat room's members. But what it doesn't allow is to observe .childChanged for the chatrooms of which I am a member. If I try to set up an observer for chats where members/myId = true, then I get the unspecified index error. How would you go about this? I have a question posted here: //allinonescript.com/questions/47769044/… – lusus_vir

Not the answer you're looking for? Browse other questions tagged or ask your own question.