1

This is more of a theoretical how database should be setup, and less about programming.

Lets say I have a news feed full of cards, which each contain a message and a like count. Each user is able to like a mesesage. I want it to be displayed to a user if they have already liked that particular card. (The same way you can see the post you like on facebook, even if you come back days later)

How would you implement that with this Firestore type database? Speed is definitely a concern..

storying it locally isn't an option, my guess would be on each card object, you would have to reference a collection that just kept a list of people who liked it. The only thing is that is a lot more querying.. which feels like it would be slow..

is there a better way to do this?

jdoej
  • 723
  • 1
  • 6
  • 10
  • I think no. You need to save LIKE/DISLIKE count at related collections document. And you may trigger write&update of this location with firebase functions. – Cappittall Dec 27 '17 at 20:01
  • confused what you mean, Like and Dislike count could be saved.. I am trying to solve the issue for a specific user.. and being able to tell if that user has liked that status – jdoej Dec 27 '17 at 21:08
  • I am sorry that I understood you wrong that you search a way without saving. But u meant locally storage. – Cappittall Dec 28 '17 at 09:55

1 Answers1

0

TL;DR

This approach requires more to setup, ie a cron service, knowledge of Firestore Security Rules and Cloud Functions for Firebase. With that said, the following is the best approach I've come up with. Please note, only pseudo-rules that are required are shown.

Firestore structure with some rules

/*
  allow read
  allow update if auth.uid == admin_uid and the
      admin is updating total_likes ... 
 */
messages/{message_key} : {
  total_likes: <int>,
  other_field:
  [,...]
}

/*allow read
  allow write if newData == {updated: true} and
      docId exists under /messages
 */
messages_updated/{message_key} : {
  updated: true
}

/*
  allow read
  allow create if auth.uid == liker_uid && !counted && !delete and
      liker_uid/message_key match those in the docId...
  allow update if auth.uid == admin_uid && the admin is
      toggling counted from false -> true ...
  allow update if auth.uid == liker_uid && the liker is
      toggling delete ...
  allow delete if auth.uid == admin_uid && delete == true and
      counted == true 
 */
likes/{liker_uid + '@' + message_key} : {
  liker_uid:,
  message_key:,
  counted: <bool>,
  delete: <bool>,
  other_field:
  [,...]
}

count_likes/{request_id}: {
  message_key:,
  request_time: <timestamp>
}

Functions

Function A

Triggered every X minutes to count message likes for potentially all messages.

  1. query /messages_updated for BATCH_SIZE docs
  2. for each, set its docId to true in a local object.
  3. go to step 1 if BATCH_SIZE docs were retrieved (there's more to read in)
  4. for each message_key in local object, add to /count_likes a doc w/ fields request_time and message_key.

Function B

Triggered onCreate of count_likes/{request_id}

  1. Delete created docs message_key from /messages_updated.
  2. let delta_likes = 0
  3. query /likes for docs where message_key == created docs message_key and where counted == false.
  4. for each, try to update counted to true (in parallel, not atomically)
    • if successful, increment delta_likes by 1.
  5. query /likes for docs where message_key == created docs message_key, where delete == true and where counted == true.
  6. for each doc, try to delete it (in parallel, not atomically)
    • if successful, decrement delta_likes by 1
  7. if delta_likes != 0, transact the total likes for this message under /messages by delta_likes.
  8. delete this doc from /count_likes.

Function C (optional)

Triggered every Y minutes to delete /count_likes requests that were never met.

  1. query docs under /count_likes that have request_time older than Z.
  2. for each doc, delete it.

On the client

  • to see if you liked a message, query under /likes for a doc where liker_uid equals your uid, where message_key equals the message's key and where delete == false. if a doc exists, you have liked it.
  • to like a message, batch.set a like under /likes and batch.set a /messages_updated. if this batch fails, try a batch_two.update on the like by updating its delete field to false and batch_two.set its /messages_updated.
  • to unlike a message, batch.update on the like by updating its delete field to true and batch.set its /messages_updated.

Pros of this approach

  1. this can be extended to counters for other things, not just messages.
  2. a user can see if they've liked something.
  3. a user can only like something once.
  4. a user can spam toggle a like button and this still works.
  5. any user can see who's liked what message by querying /likes by message_key.
  6. any user can see all the messages any user has liked by querying /likes by liker_uid.
  7. only a cloud function admin updates your like counts.
  8. if a function is fired multiple times for the same event, this function is safe, meaning like counts will not be incremented multiple times for the same like.
  9. if a function is not fired for some event, this approach still works. It just means that the count will not update until the next time someone else likes the same message.
  10. likes are denormalized to only one root level collection, instead of the two that would be required if you had the like under the the message's likes subcollection and under the liker's messages_liked subcollection.
  11. like counts for each message are updated in batches, ie if something has been liked 100 times, only 1 transaction of 100 is required, not 100 transactions of 1. this reduces write rate conflicts due to like counter transactions significantly.

Cons of this approach

  1. Counts are only updated however often your cron job fires.
  2. Relies on a cron service to fire and in general there's just more to set up.
  3. Requires the function to authenticate with limited privileges to perform secure writes under /likes. In the Realtime Database this is possible. In Firestore, it's possible, but a bit hacky. If you can wait and don't want to use the hacky approach, use the regular unrestricted admin in development until Firestore supports authenticating with limited privileges.
  4. May be costly depending on your standpoint. There are function invocations and read/write counts you should think about.

Things to consider

  1. When you transact the count in Function B, you may want to consider trying this multiple times in case the max write rate of 1/sec is exceeded and the transaction fails.
  2. In Function B, you may want to implement batch reading like in Function A if you expect to be counting a lot of likes per message.
  3. If you need to update anything else periodically for the message (in another cron job), you may want to consider merging that function into Function B so the write rate of 1/sec isn't exceeded.
Vincent
  • 1,553
  • 1
  • 11
  • 21