Firebase:如何以事务方式更新多个节点?斯威夫特 3 [英] Firebase: How to update multiple nodes transactionally? Swift 3

查看:25
本文介绍了Firebase:如何以事务方式更新多个节点?斯威夫特 3的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在开发一个专为清洁服务设计的应用程序.在此应用程序中,员工(清洁工)可以阅读多个客户(用户)所做的工作(预订)列表.

所有清洁工都可以读取用户节点中的所有预订.最初,当用户将预订保存在数据库中时,键 claimed: 的值为 false",这意味着它没有被清洁工认领.

每当清洁工想要声明列表中的工作时,他都必须触摸一个按钮,该按钮将向 Firebase 数据库发出请求,以将键 claimed 的值修改为 true 在路径 /Users/UID/bookings/bookingNumber

一次只能允许一名清洁工修改 claimed 键的值.如果允许多个清洁工修改 claimed 键的值,其他清洁工最终会声明相同的工作.我们不希望这种情况发生.

此外,在清洁器将 claimed 键的值修改为 true 后,我们将需要再次请求路径 CLeaners/UID/bookings/bookingNumber 为了保存他刚刚在清洁工节点中声明的预订.
- 根据 firebase 文档,每当我们希望一次仅通过一个请求修改资源时,我们就会使用事务,如果有多个并发请求尝试写入同一资源,其中一个会成功.但是使用事务的问题在于它允许写入仅一条路径,而不能写入多条路径.

如何确保即使多个用户可以读取此路径/Users/UID/bookings/bookingNumber,但一次只有一个用户可以更新它?并且如果写入成功,进一步写入第二条路径Cleaners/UID/bookings/bookingNumber.

我们需要考虑到客户端的互联网连接可能会断开,用户可能会退出应用程序,或者只是手机会在写入上述指定路径之间的任何时间意外关闭.

数据库结构如下

根清洁工用户识别码预订订单号数量:10"声称:真实"用户用户识别码其他ID订单号数量:10"声称:真实"订单号金额:50"声称:假的"

为了避免任何覆盖,我决定使用 Firebase 事务.我可以写入单个节点作为事务,但写入完成处理程序中的第二个节点不是解决方案,因为清洁器的互联网连接可能会断开或应用程序可能会在收到服务器响应之前退出,因此完成处理程序 {(error, commit,snapshot) in.... 不会被评估,第二次写入不会成功.

另一种情况是:执行第一次写入,
1. 在客户端应用程序中收到响应
2. 客户端应用程序中尚未收到响应
用户立即退出应用程序.在这种情况下,第二次写入将永远不会执行,因为在完成处理程序中收到(或未收到)响应后还没有评估任何代码,从而使我的数据库处于不一致状态.

<小时>

来自 Firebase 文档:

<块引用>

交易不会在应用重启后持续

即使启用了持久性,事务也不会跨应用程序重新启动.所以你不能依赖离线完成的交易致力于您的 Firebase 实时数据库.

  • 是否可以在 Swift 中使用 Firebase 事务写入 Firebase 数据库中的多个节点?

如果是这样,我该怎么做?我在 google https://firebase.googleblog.com/2015/09/introducing-multi-location-updates-and_86.html .我知道你可以原子地写入多个节点,但我想写为事务.我试图写入 else 子句中的两个节点,但我在这一行收到警告 let updated = updateInUsersAndCleaners as?FIRMutableData

<块引用>

从 '[FIRDatabaseReference : FIRMutableData]' 转换为不相关的类型'FIRMutableData' 总是失败

 class ClaimDetail: UIViewController,UITableViewDelegate,UITableViewDataSource {var valueRetrieved = [String:AnyObject]()变量 uid:字符串?@IBAction func claimJob(_ sender: Any) {dbRef.runTransactionBlock({ (_ currentData:FIRMutableData) -> FIRTransactionResult in//如果 valueRetrieved 为 nil 则中止守卫让 val = currentData.value 作为?[字符串:AnyObject] 其他 {返回 FIRTransactionResult.abort()}self.valueRetrieved = val守卫让 uid = FIRAuth.auth()?.currentUser?.uid else {返回 FIRTransactionResult.abort()}self.uid = uid对于 self.valueRetrieved.keys 中的键 {打印(键是(键)")//解包声明"键的值守卫让 keyValue = self.valueRetrieved["Claimed"] as?字符串其他{返回 FIRTransactionResult.abort()}//检查键值是否为真如果键值 == 真"{//预订已经分配,​​中止返回 FIRTransactionResult.abort()} 别的 {//将新值写入firebase让 newData = self.createDictionary()currentData.value = newData让 usersRef = self.dbRef.child("Users").child(FullData.finalFirebaseUserID).child(FullData.finalStripeCustomerID).child(FullData.finalBookingNumber)让cleanersRef = self.dbRef.child("Cleaners").child(self.uid!).child("bookings").child(FullData.finalBookingNumber)//为两个节点创建我们要更新的数据让 updateInUsersAndCleaners = [usersRef:currentData,cleanersRef:currentData]让更新 = updateInUsersAndCleaners 作为?FIRMutableData返回 FIRTransactionResult.success(withValue:更新!)}//else结束}//结束for key in self返回 FIRTransactionResult.abort()}) {(错误,提交,快照)在如果让错误 = 错误 {//显示错误提示,要求用户重试self.alertText = "无法申请预订,请重试."self.alertActionTitle = "OK"self.segueIdentifier = "unwindfromClaimDetailToClaim"self.showAlert()}否则如果提交==真{self.alertText = "已预订.请检查您的日历"self.alertActionTitle = "OK"self.segueIdentifier = "unwindfromClaimDetailToClaim"self.showAlert()}}}//claimJob 按钮结束}//课程结束扩展声明详细信息{//向用户显示警报并继续声明 tableView功能显示警报(){让 alertMessage = UIAlertController(title: "", message: self.alertText, preferredStyle: .alert)alertMessage.addAction(UIAlertAction(title: self.alertActionTitle, style: .default, handler: { (action:UIAlertAction) inself.performSegue(withIdentifier: self.segueIdentifier, sender: self)}))self.present(alertMessage,动画:true,完成:nil)}//使用从完成处理程序接收到的数据和新数据创建字典func createDictionary() ->任何对象{让 timeStamp = Int(Date().timeIntervalSince1970)self.valueRetrieved["CleanerUID"] = uid 作为 AnyObject?self.valueRetrieved["TimeStampBookingClaimed"] = timeStamp as AnyObject?self.valueRetrieved["Claimed"] = "true" as AnyObject?打印(第 89 行扩展声明详细信息")返回 self.valueRetrieved as AnyObject}}//扩展结束 ClaimDetail

解决方案

在 Firebase 文档的 启用离线功能 指定:

<块引用>

事务不会在应用程序重启后保持不变
即使启用了持久性,事务也不会跨应用程序重新启动.
所以你不能依赖离线完成的交易致力于您的 Firebase 实时数据库.

因此:
1. 无法在客户端使用 firebase 事务来更新两个或多个路径上的值.
2. 使用完成回调来执行第二次写入是不可行的,因为客户端可能会在完成处理程序中收到来自 firebase 服务器的响应之前重新启动应用程序,从而使数据库处于不一致状态.

我假设我唯一的选择是使用 基于 REST 的条件请求,如 Firebase 文档中所述.
这将实现 Firebase 框架为 IOS 客户端提供的相同功能.

    1. 客户端将通过 Alamofire 向我的服务器发出请求(我将使用 Vapor 框架以利用 Swift 语言),一旦 Vapor 服务器收到请求,将向 Firebase 数据库服务器发送 GET 请求root/users/bookings/4875383 我将在其中请求 ETAG_VALUE

ETAG 值是什么?:(一个唯一标识符,每次在发出 GET 请求的路径上数据更改时都会有所不同.即,如果另一个用户之前写入同一路径我的写请求资源成功,我的写操作将被拒绝,因为其他用户的写操作已经修改了路径上的 ETAG 值.这将使用户能够以事务方式将数据写入路径)

    1. 收到来自 Firebase 服务器的包含 ETAG_VALUE 的响应.

    1. 向 Firebase 服务器发出 PUT 请求,并在标头中指定从上一个 GET 请求收到的 ETag: [ETAG_VALUE].如果发布到服务器的 ETAG 值与 Firebase 服务器上的值匹配,则写入将成功.如果该位置不再与 ETag 匹配(如果另一个用户向数据库写入新值可能会发生这种情况),则请求将失败而不写入该位置.返回响应包括新值和 ETag.

    1. 此外,现在我们可以更新 root/Cleaners/bookings/4875383 中的值以反映清洁工声称的工作.

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

解决方案

In the Firebase documentation in the section Enable Offline Capabilities it is specified that:

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.

Therefore:
1. there is no way to use firebase transactions at client side to update a value at two or more paths.
2. using a completion callback to perform the second write is not viable since the client could restart the app before a response is received in the completion handler from the firebase server, thus leaving the database in inconsistent state.

I assume that my only option to update data transactionally at the first path and further update the second path with that data that was already written at the first path in Firebase Database, would be to use Conditional Requests over REST as specified in the Firebase Documentation.
This would achieve the same functionality provided by Firebase framework for IOS clients.

    1. The client will make a request via Alamofire to my server (I will use Vapor framework so as to leverage the Swift language), once the request is received at the Vapor server, a GET request will be sent to the Firebase Database server root/users/bookings/4875383 in which I will request an ETAG_VALUE

What is ETAG Value?: ( a unique identifier which will be different every time data changes at the path where GET Request is made. Namely if another user writes to the same path before my write request the resource succeeds, my write operation will be rejected since the ETAG value at the path will have already been modified by the other user's write operation. This would enable users to write data transactionally to a path)

    1. a response is received from the Firebase Server containing a an ETAG_VALUE.

    1. make a PUT request to the Firebase Server and in the header specify the ETag: [ETAG_VALUE] received from the previous GET request. If the ETAG value posted to the server matches the value at the Firebase Server, the write will succeed. If the location no longer matches the ETag, which might occur if another user wrote a new value to the database, the request fails without writing to the location. The return response includes the new value and ETag.

    1. Furthermore, now we can update the value at root/Cleaners/bookings/4875383 to reflect the job that was claimed by a cleaner.

这篇关于Firebase:如何以事务方式更新多个节点?斯威夫特 3的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆