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