skip to Main Content

I am using Firestore to build a social media app, I am wondering what is the best way to structure the data for friend requests is.

When a user John wants to friend request Mary and Anne, Do I create a root collection called friend_requests and add 3 documents:

  1. A document with John’s userId as a key the value of which will be an object each for both Mary and Anne’s containing their userId and the status of request (pending/accepted/declined)
  2. A document with Mary’s userId as a key and an array with an object with John’s userId and status of request
  3. A document with Annes’s userId as a key and an array with an object with John’s userId and status of request

Now, when Anne and Mary accept the friend request, I delete all three documents in friend_requests collection and then:

  • I add a subcollection for each user (in users root collection) called friends that contains usersIds, displayNames, and photoURLs of friends that have accepted requests

or

  • Should I make another root collection friends that has a document for each user containing the friends information instead?

It seems like a lot of duplicate data so I’m trying to wrap my head around it and find the best and most efficient way to do it in Firestore. Any advice would be much appreciated and maybe I am going about it the wrong way!

2

Answers


  1. The first approach seems to have a lot of document creations and deletions. Instead you could create a sub-collection "friends" under "users" and store a field "status". So the structure would be:

    users -> {userId} -> friends -> {friendId}
    

    When User2 sends a friend request to User1, you can add a friend sub-document under User1 and set the status field to requested along with friendId. Once User1 accepts the request, just update the status to active. This will reduce costs of that 1 delete and 1 create operations per friend request.

    This should cover many use cases including:

    1. Listing friends/friend request of a users
    2. Listing pending friend requests sent by me (a collection group query can be used if you use sub-collection)

    You can use Firestore triggers for Cloud Functions if you need to process anything like updating database, sending a notification/e-mail, etc when the friend request has been accepted.


    The friends sub-collection could be a root-level collection but in that case you’ll have to add another field userId to filter of friends of a given user. That doesn’t make a big difference but checkout What are the benefits of using a root collection in Firestore vs. a subcollection? for more information on that.

    Login or Signup to reply.
  2. While Dharmaraj’s solution will certainly work, I think that there is an even simpler solution for that, that can help you reduce the number of reads a little bit. So let’s assume userOne sends a friend request to userTwo, and userThree sends a friend request to the userOne.

    Firestore-root
      |
      --- users (collection)
      |    |
      |    --- $userOneUid (document)
      |    |     |
      |    |     --- //User data
      |    |
      |    --- $userTwoUid (document)
      |    |     |
      |    |     --- //User data
      |    |
      |    --- $userThreeUid (document)
      |          |
      |          --- //User data
      |
      --- requests (collection)
           |
           --- $userOneUid (document)
           |     |
           |     --- ownRequests (map)
           |     |      |
           |     |      --- $userTwoUid
           |     |             |
           |     |             --- displayName: "User Two"
           |     |             |
           |     |             --- photoUrl: "https://"
           |     |             |
           |     |             --- status: "requested"
           |     |
           |     --- friendRequests (map)
           |     |      |
           |     |      --- $userThreeUid
           |     |             |
           |     |             --- displayName: "User Three"
           |     |             |
           |     |             --- photoUrl: "https://"
           |     |             |
           |     |             --- status: "requested"
           |     |
           |     --- allFriends (map)
           |            |
           |            --- //All friends
           |
           --- $userTwoUid (document)
           |     |
           |     --- friendRequests (map)
           |            |
           |            --- $userOneUid
           |                   |
           |                   --- displayName: "User One"
           |                   |
           |                   --- photoUrl: "https://"
           |                   |
           |                   --- status: "requested"
           |
           --- $userThreeUid (document)
                 |
                 --- ownRequests (map)
                        |
                        --- $userOneUid
                               |
                               --- displayName: "User One"
                               |
                               --- photoUrl: "https://"
                               |
                               --- status: "requested"
    

    To have a flattened schema, instead of creating nested collections or nested maps under the User object, I have created a separate top-level collection called requests. Inside this collection, there will be documents that will manage the user’s own requests, friend requests, and all friends.

    Seeing this schema, you might think, oh, but how can I manage to add such an amount of data in a single document? Even if we’re limited to storing up to 1 Mib of storage in a single document, when it comes to storing such simple objects, we can store pretty much. However, if you’re worried about the space, then you should consider creating separate collections for each situation, ownRequests, friendRequests, and allFriends. In this case, for simplicity, let’s assume all the data fits into a single document.

    Going forward, as you can see, we have stored the minimum data from a User object under each map, where the key is the UID and the value is represented by three fields. In this way you can display the name and the profile photo, to clearly indicate the user who performed the friend request.

    Now, when userOne sends a friend request to userTwo, there are two operations that need to be done. The first one would be to add userOne inside the friendRequests map of userTwo and the second one would be to add userTwo under its own ownRequests map.

    Between these two friends we have now a friend request, this means that you’ll always have to make sure that you’ll restrict userTwo from sending friend requests to userOne. How can you do that? Simply by checking the existence of the $userTwoUid inside ownRequests of userOne. If you want the userOne to be able to cancel the friend request, or userTwo to reject the friend request, then simply revert the above two operations.

    The same rule applies in the case userThree.

    Regarding displaying data, if you want to show a list of your own requests, friend requests, or a list of all friends, you need to make a single database call, that costs a single read. If you however need to load more data of a user, then when you click on the user, you can perform another request and load all the data of the user, since you already know the UID.

    When userTwo accepts the friend request from userOne, simply move their objects inside allFriends map. You can also go ahead and add a bannerFriends if you want, to prevent some other users from sending friend requests. Please also note, that this will also cost only a single read.

    It seems like a lot of duplicate data so I’m trying to wrap my head around it and find the best and most efficient way to do it in Firestore.

    Denormalizing data it’s a quite common practice when it comes to NoSQL databases like Firestore and for that I recommend you check my answer from the following post:

    Where you’ll also be able to see other solutions for structuring such a database.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search