0

I have 2 questions related to Firebase's transaction in the real-time database. It will be easier to explain with an example. This is just an example, not my real code. So, do not worry if there are some compile errors.

Let say I have a building. In this building, there are have some data. I have an array of floors. Each floor can have a counter of how many chairs there are on this floor. A client can have a lot of floors, so I do not want to load all of them. I just load the ones I need for this client. I need to know how many chairs there are in total even if I do not load them all. The rules can look like this:

"...": 
{

    "building":
    {
        "...": 
        {

        },
        
        "totalNbChairs": 
        {   
          ".validate": "newData.isNumber()"
        },
        
        "floors": 
        {  
             "$floorId": 
            {
                "nbChairs": 
                {   
                  ".validate": "newData.isNumber()"
                },
                
                "...": 
                {

                },
            },
        },
    },
},

As I said, this is just an example, not my actual code. DO not worry about code issues.

My clients can connect on multiple devices, so I need to use transactions to adjust the "totalNbChairs" when a floor changes his "nbChairs". Important, I need to set the actual number of chairs on the floor, not just decrease a value. If the "nbChairs" is 10 for a floor and the client set "8" on 2 devices at the same time, I can not do "-2" on both devices at the same time.

The transaction will look like this

void SetNbChair(String floorId, long nbChairsToSet){
    FirebaseDatabase.getInstance().getReference()
    .child("...")
    .child("building")
    .runTransaction(new Transaction.Handler() {
            @Override
            public Transaction.Result doTransaction(MutableData mutableData) {
                
                //first I need to know how many chairs I have right now on the floor
                MutableData nbChairMutableData = mutableData.child("floors").child(floorId).child("nbChairs");
                Long nbChairLong = (Long)nbChairMutableData.getValue();
                long nbChair = 0;
                if(nbChairLong != null){
                    nbChair = nbChairLong;
                }
                
                long diff = nbChairsToSet - nbChair;
                
                //now I can update the number of chair in the floor
                nbChairMutableData.setValue(nbChairsToSet);
                
                //Update the totalNbChairs
                MutableData totalNbCHairsMutableData = mutableData.child("totalNbChairs");
                Long previousTotalChairLong = (Long)totalNbCHairsMutableData.getValue();
                long totalChair = 0;
                if(previousTotalChairLong != null){
                    totalChair = previousTotalChairLong;
                }
                
                totalChair += diff;
                
                //update the value
                totalNbCHairsMutableData.setValue(totalChair);
                
            }
            
             @Override
            public void onComplete(DatabaseError databaseError, boolean committed,
                                   DataSnapshot currentData) {

                ...
            }
        });
}

My first question is: When are downloaded the data I need to get on the client side? Because for what I see, 2 things can happen.

First, when I do this

FirebaseDatabase.getInstance().getReference()
    .child("...")
    .child("building")
    .runTransaction

It's possible Firebase downloads everything in (".../building"). If this is the case, this is pretty bad for me. I do not want to download everything. So if all the data is downloaded for this transaction, this is really bad. If this is the case, does anyone have an idea how I can do this transaction without download all the floors?

But, it's also possible Firebase downloads the data when I do this

Long nbChairLong = (Long)nbChairMutableData.getValue();

In this case, it's far better but not perfect. In this case, I need to download "nbChairs". Wait to get it. After, I need to download "totalNbChairs" and wait to get it. If firebase downloads the data when we do a getValue(). Can we batch all the getValue I need in a single call to avoid waiting to download twice?

But I may be wrong and firebase does something else. Can someone explain to me when and what firebase downloads to the client so I will not have a huge surprise?

Now the second question. I implemented the version I show. But I can not use it before I know the answer to my first question. But, I still did some tests. Pretty fast, I found out my "transaction" callback got some "null" event if there is data in the database. Ok, the documentation said it was expected behavior. Ok, no problem with that. I protected my code and I have something like this

if(myMandatoryData == null){
    return Transaction.success(mutableData);
}

and, yes, the first time, my early return is called and the function is recalled. The data is valid the second time. Ok, seems fine, but... and a BIG BUT! I noticed something pretty bad. Something to mention, I have some ValueEventListener active to know when the data changed in the database, So I have some stuff like this

databaseReference.addValueEventListener( new ValueEventListener(){

            @Override
            public void onDataChange(DataSnapshot dataSnapshot) {
                    ...
           
            }

            @Override
            public void onCancelled(DatabaseError databaseError) {
                ...
            }
        } );

After my early return, every "onDataChange" on every ValueEventListener is called with "null". So, my code in the callback handles this like if the data was deleted. So, I got an unexpected result in my Ui, it's like someone deleted all my data. When the transaction retries and has the data, the "onDataChange" is recalled with the valid data. But until it does, my UI just shows like there is nothing in the database. Am I supposed to cancel every ValueEventListener when I start a simple transaction? This seems pretty bad. I do not want to cancel them all. Also, I do not want to redownload all the data after I restart them when the transaction is done. I do not want to add a hack to ignore deleted data while a transaction is running in every ValueEventListener. I can miss if some data is really deleted from another device when the transaction is running. What Am I supposed to do at this point?

Thanks

qqchose
  • 7
  • 3

1 Answers1

0

When you execute this code:

FirebaseDatabase.getInstance().getReference()
    .child("...")
    .child("building")
    .runTransaction

Firebase will read all data under the .../building path. For this reason it is recommended to run transactions as low in the JSON tree as possible. If this is not an option for you, consider running the transaction in something like Cloud Functions (maybe even with maxInstances set to 1) to reduce the contention on the transaction.


To you second question: this is the expected behavior. Your transaction handler is immediately called with the client's best guess to the current value of the node, which in most cases will be null. While this may never be possible in your use-case, you will still have to handle the null by returning a value.

For a longer explanation of this, see:

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • Thanks for the reply. SO I can not just use the transaction. I'm still using the free version. But if my app becomes popular, I will use the cloud function. But, if firebase programmers do not know what to do, adding a way to set a list of data paths we will want to read and write before starting a transition will be pretty usefull. – qqchose Oct 14 '21 at 12:37
  • Agreed that would be useful I recommend [filing a feature request](https://firebase.google.com/support/contact/bugs-features/) for it. – Frank van Puffelen Oct 14 '21 at 14:22
  • done, but like you said, me reputation is too low to show the arrow up – qqchose Oct 20 '21 at 12:40