I am developing an application designed for cleaning services. In this application the employees (cleaners) can read a list of jobs (bookings) which have been made by multiple customers (Users).
All cleaners can read all bookings in Users node. Initially, when a booking is saved in the database by a user, the key claimed: has a value of “false”,meaning it has not been claimed by a cleaner.
Whenever a cleaner wants to claim a job present in the list, he will have to touch a button which will make a request to Firebase Database to modify the value of key claimed to true at path /Users/UID/bookings/bookingNumber
Only one cleaner at a time should be allowed to modify the value of claimed key. If multiple cleaners were allowed to modify the value of claimed key, other cleaners would end up claiming the same job. We don't want that to happen.
Furthermore, after a cleaner modifies the value of claimed key to true, we will need to make another request to path CLeaners/UID/bookings/bookingNumber in order to save the booking he has just claimed in the cleaners node.
- According to the firebase docs, we use transactions whenever we want a resource to be modified by only one request at a time, if there are multiple concurrent requests trying to write to the same resource, one of them will succeed.
But the problem with using transactions is that it enables writing to only one path, it does not enable writing to multiple paths.
How can I ensure that even though multiple users can read this path /Users/UID/bookings/bookingNumber, only one user at a time can update it? And if the write is successful, further write to the second path Cleaners/UID/bookings/bookingNumber.
We need to take into account that the client's internet connection can drop, the user can quit the app, or simply the phone will switch off unexpectedly any time in-between writing to the paths specified above.
The database structure is as follows
Root
Cleaners
UID
bookings
bookingNumber
amount: “10”
claimed: “true”
Users
UID
otherID
bookingNumber
amount: “10”
claimed: “true”
bookingNumber
amount: “50”
claimed: “false”
To avoid any overwrites, I have decided to use Firebase transactions. I can write to a single node as transaction, but writing to the second node in the completion handler is not a solution since the cleaner's internet connection may drop or app could be quit before a response is received from the server, thus the code in the completion handler {(error, committed,snapshot) in....would not be evaluated and the second write would not succeed.
Another scenario would be: first write is executed,
1. response is received in client app
2. response is not yet received in client app
and the user quits the app immediately. In this case the second write will never be executed since no code is yet evaluated after response is received (or not) in the completion handler, thus leaving my database in an inconsistent state.
From Firebase docs:
Transactions are not persisted across app restarts
Even with persistence enabled, transactions are not persisted across app restarts. So you cannot rely on transactions done offline being committed to your Firebase Realtime Database.
Is it possible to write to multiple nodes in a Firebase Database using Firebase Transactions in Swift?
If so, how can I do this? I see no example in this blog from google https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html . I do understand that you can write atomically to multiple nodes, but I'd like to write as transaction.
I am trying to write to two nodes in the else clause, but I get a warning on this line let updated = updateInUsersAndCleaners as? FIRMutableData
Cast from '[FIRDatabaseReference : FIRMutableData]' to unrelated type 'FIRMutableData' always fails
class ClaimDetail: UIViewController,UITableViewDelegate,UITableViewDataSource {
var valueRetrieved = [String:AnyObject]()
var uid:String?
@IBAction func claimJob(_ sender: Any) {
dbRef.runTransactionBlock({ (_ currentData:FIRMutableData) -> FIRTransactionResult in
//if valueRetrieved is nil abort
guard let val = currentData.value as? [String : AnyObject] else {
return FIRTransactionResult.abort()
}
self.valueRetrieved = val
guard let uid = FIRAuth.auth()?.currentUser?.uid else {
return FIRTransactionResult.abort()
}
self.uid = uid
for key in self.valueRetrieved.keys {
print("key is \(key)")
//unwrap value of 'Claimed' key
guard let keyValue = self.valueRetrieved["Claimed"] as? String else {
return FIRTransactionResult.abort()
}
//check if key value is true
if keyValue == "true"{
//booking already assigned, abort
return FIRTransactionResult.abort()
} else {
//write the new values to firebase
let newData = self.createDictionary()
currentData.value = newData
let usersRef = self.dbRef.child("Users").child(FullData.finalFirebaseUserID).child(FullData.finalStripeCustomerID).child(FullData.finalBookingNumber)
let cleanersRef = self.dbRef.child("Cleaners").child(self.uid!).child("bookings").child(FullData.finalBookingNumber)
//Create data we want to update for both nodes
let updateInUsersAndCleaners = [usersRef:currentData,cleanersRef:currentData]
let updated = updateInUsersAndCleaners as? FIRMutableData
return FIRTransactionResult.success(withValue: updated!)
}//end of else
}//end of for key in self
return FIRTransactionResult.abort()
}) {(error, committed,snapshot) in
if let error = error {
//display an alert with the error, ask user to try again
self.alertText = "Booking could not be claimed, please try again."
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
} else if committed == true {
self.alertText = "Booking claimed.Please check your calendar"
self.alertActionTitle = "OK"
self.segueIdentifier = "unwindfromClaimDetailToClaim"
self.showAlert()
}
}
}//end of claimJob button
}//end of class
extension ClaimDetail {
//show alert to user and segue to Claim tableView
func showAlert() {
let alertMessage = UIAlertController(title: "", message: self.alertText, preferredStyle: .alert)
alertMessage.addAction(UIAlertAction(title: self.alertActionTitle, style: .default, handler: { (action:UIAlertAction) in
self.performSegue(withIdentifier: self.segueIdentifier, sender: self)
}))
self.present(alertMessage, animated: true,completion: nil)
}
//create dictionary with data received from completion handler and the new data
func createDictionary() -> AnyObject {
let timeStamp = Int(Date().timeIntervalSince1970)
self.valueRetrieved["CleanerUID"] = uid as AnyObject?
self.valueRetrieved["TimeStampBookingClaimed"] = timeStamp as AnyObject?
self.valueRetrieved["Claimed"] = "true" as AnyObject?
print("line 89 extension CLaim Detail")
return self.valueRetrieved as AnyObject
}
} // end of extension ClaimDetail