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

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

问题描述

我正在开发一个专门用于清洁服务的应用程序。在这个应用程序中,员工(清洁工)可以读取多个客户(用户)所做的工作清单(预订)。

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

每当清洁工想要求职时,他都可以在列表中看到,他必须触摸一个按钮,该按钮会向Firebase数据库发出请求,以修改在 / Users / UID / bookings / bookingNumber 真 $ c
$ b

每次只允许一个清理者修改声明的值键。如果允许多个清洁工修改所声称的键的值,其他清洁工最终会声称做同样的工作。我们不希望这样的事情发生。

此外,清理程序将声明的键的值修改为 true ,我们将需要再次请求路径 CLeaners / UID / bookings / bookingNumber ,以便保存他在清洁工节点声明的预订。
- 根据firebase文档,我们使用事务,只要我们希望一次只有一个请求修改资源,如果有多个并发请求试图写入相同的资源,其中一个成功。
但是使用事务的问题是它只能写入一个路径,不能写到多个路径

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



我们需要考虑到客户的互联网连接可能会丢失,用户可以退出应用程序,或者只是在写入上面指定的路径之间的任何时候,电话会意外关闭。



数据库结构如下:

$ root
清理
UID
预订
bookingNumber
amount:10
声明:true


用户
UID
otherID
bookingNumber
数量:10
声明:true

bookingNumber
数量:50
声明:false

为了避免重写,我决定使用Firebase交易。我可以作为事务写入单个节点,但是写入完成处理程序中的第二个节点并不是一个解决方案,因为清理程序的Internet连接可能会丢失,或者在从服务器收到响应之前可能会退出应用程序,因此,完成处理程序 {(error,committed,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'失败




     类ClaimDetail:UIViewController,UITableViewDelegate,UITableViewDataSource {

    var valueRetrieved = [String:AnyObject]()
    var uid:String?

    @IBAction func claimJob(_ sender:Any){

    dbRef.runTransactionBlock({(_ currentData:FIRMutableData) - > FIRTransactionResult in

    //如果valueRetrieved是nil abort
    guard let val = currentData.value as?[String:AnyObject] else {
    return FIRTransactionResult.abort()
    }
    self.valueRetrieved = val
    $ b guard let uid = FIRAuth.auth()?currentUser?.uid else {
    return FIRTransactionResult.abort()
    }
    self.uid = uid

    for self.valueRetrieved.keys {
    print(key is \(key))

    //展开'Claimed' key
    guard let keyValue = self.valueRetrieved [Claimed] as String else {
    return FIRTransactionResult.abort()
    }

    //检查key值为真
    if keyValue ==true{

    //预定已经分配,​​中止
    返回FIRTransactionResult.abort()

    } else {
    //将新值写入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)

    //为两个节点创建要更新的数据
    let updateInUsersAndCleaners = [usersRef:currentData,cleanersRef:currentData]
    let updated = updateInUsersAndCleaners as? FIRMutableData
    返回FIRTransactionResult.success(withValue:updated!)

    } // end of else
    } //结束for self中的键

    返回FIRTransactionResult.abort()
    )){(错误,提交,快照)在

    如果让错误=错误{
    //显示警告与错误,询问用户再试一次

    self.alertText =预订无法获取,请重试。
    self.alertActionTitle =OK
    self.segueIdentifier =unwindfromClaimDetailToClaim
    self.showAlert()

    } else if committed == true {

    self.alertText =预订声明。请检查您的日历
    self.alertActionTitle =OK
    self.segueIdentifier =unwindfromClaimDetailToClaim
    self.showAlert()



    // end of claimJob按钮

    } //课程结束


    扩展ClaimDetail {

    //显示警告给用户,然后继续声明tableView
    func showAlert(){
    let alertMessage = UIAlertController(title:,message:self.alertText在
    中使用UIAlertAction(title:self.alertActionTitle,style:.default,handler:{(action:UIAlertAction))self.performSegue(withIdentifier:self.segueIdentifier,发件人:自己)
    }))
    self.present(alertMessage,animated:true,completion:nil)
    }


    //从完成处理函数新数据
    func createDictionary() - > AnyObject {
    let timeStamp = Int(Date()。timeIntervalSince1970)
    self.valueRetrieved [CleanerUID] = uid as AnyObject?
    self.valueRetrieved [TimeStampBookingClaimed] = timeStamp as AnyObject?
    self.valueRetrieved [Claimed] =true作为AnyObject?
    print(line 89 extension CLaim Detail)
    return self.valueRetrieved as AnyObject
    }
    } //延期结束ClaimDetail


    解决方案

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

    < blockquote>

    应用程序重新启动时不会持久化事务

    即使启用了持久性,事务也不会持续存在于
    应用程序重新启动期间。

    因此,您不能依赖于离线完成的交易,将
    交给您的Firebase实时数据库。

    因此:

    1 。在客户端无法使用firebase事务来更新两个或更多路径的值。

    2.使用完成回调执行第二次写入不是vi因为客户端可以在firebase服务器的完成处理程序收到响应之前重新启动应用程序,从而使数据库处于不一致状态。



    我假设我的唯一选项可以在第一个路径上事务性地更新数据,并使用Firebase数据库中第一个路径中已写入的数据进一步更新第二个路径,即可使用 Firebase文档中指定的对REST的条件请求
    这将实现IOS的Firebase框架提供的相同功能客户端。




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




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



        1. 从包含ETAG_VALUE的Firebase服务器收到响应。



          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 he can see 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 resources 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 insure 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:如何以事务方式更新多个节点? Swift 3的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

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