Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Proper design of a starring/liking system in Firebase: Should I use transactions?

I'm working on an app that does something similar to the star count on a blog post example given in the Firebase documentation and I have a question about the design of the polling system and whether to use .

From the example, the data for a post looks like this:

postid {
    // some data here about the post, author, id, etc,
    starCount : <an integer count of the stars>,
    stars : {
        <user who starred> : true,
        <user who starred> : true,
        // ... and so on, basically a list of all the users who starred the post
    }
}

The docs use a transaction to update the star count:

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(MutableData mutableData) {
            Post p = mutableData.getValue(Post.class);
            if (p == null) {
                return Transaction.success(mutableData);
            }

            if (p.stars.containsKey(getUid())) {
                // Unstar the post and remove self from stars
                p.starCount = p.starCount - 1;
                p.stars.remove(getUid());
            } else {
                // Star the post and add self to stars
                p.starCount = p.starCount + 1;
                p.stars.put(getUid(), true);
            }

            // Set value and report transaction success
            mutableData.setValue(p);
            return Transaction.success(mutableData);
        }

        @Override
        public void onComplete(DatabaseError databaseError, boolean b,
                               DataSnapshot dataSnapshot) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

Because of the use of a transaction both the starCount and stars seem to need to be part of the same object. Why? Because I want to use both a transaction and assure automicness. The other option for assuring automicness is multi-path updates, but as noted here in the comments you cannot use multi-path updates with transitions. You have access to the mutableData (in this case the post object) and that's it.

SO if the starCount and stars must be stored in the same location, I don't understand the point of the starCount. Whenever you get a post object, it will include stars, the list of everyone who starred it. Why not remove the starCount entirely and just get the size of the stars list? Is there something non performant about this?

In this case, starring/unstarring something would be something like p.stars.put(getUid(), true); and p.stars.remove(getUid());. If I did that, I don't see the need for a transaction, because I no longer have multiple users updating a "counter" object. I could use a transaction for the p.stars.put(getUid(), true); statement, but it seems highly unlikely to me that the same user who try to star/unstar something from two different devices, and even if they did, it wouldn't be disastrous as far as I can tell.

So what am I missing here? Why use transactions here and what benefit does storing and manually tracking starCount add? I could see a performance benefit storing the stars user list elsewhere in the database, as shown in this example, but then the transaction wouldn't work.

Any insight would be appreciated and thank you!

like image 361
Lyla Avatar asked Jan 04 '17 20:01

Lyla


People also ask

When should I use Firebase transactions?

Transactions are useful when you want to update a field's value based on its current value, or the value of some other field. A transaction consists of any number of get() operations followed by any number of write operations such as set() , update() , or delete() .

What is the correct Firebase database structure?

All Firebase Realtime Database data is stored as JSON objects. You can think of the database as a cloud-hosted JSON tree. Unlike a SQL database, there are no tables or records. When you add data to the JSON tree, it becomes a node in the existing JSON structure with an associated key.


1 Answers

In our effort to create a single working sample app from which to get the documentation samples, we indeed may have created a non-sensible (but functional) use for transactions.

I'd recommend having a look at my answer to this question for a more reasonable (but definitely complex) way of doing secure counts using multi-location updates and server-side security rules.

In general though, I'd always recommend splitting the voting logic from the counting logic in cases like this. So when a user votes for a post, you write a simple record into the database that records their action:

votesQueue
   <push id>
      <uid>: true

And then you have a tiny server script (e.g. a firebase queue worker) that listens to this queue and aggregates the number of votes.

This approach is very common when using Firebase and makes matters a lot simpler. You can probably easily imagine how you'd secure these two nodes (votesQueue and voteTotals), while I can assure you that the solution in the answer I linked above gives me quite some maintenance headaches.

like image 115
Frank van Puffelen Avatar answered Oct 15 '22 01:10

Frank van Puffelen