firebase - 如何防止 Firestore 为预订按钮写入竞争条件
问题描述
概括
我正在开发一个应用程序,用户可以在其中预订和取消课程预订。在ReservationButtonView
I 中,两个按钮分别将用户添加和删除到锻炼课程。目前,我显示的按钮基于用户的 Firebase Auth uid 是否列在 Firestore 文档中。
快速点击预订按钮时遇到问题。具体来说,reservationCnt
显示的内容多于或少于为某个课程保留的实际用户会变得不准确。
我发现解决此问题的唯一方法是使用 Firestore 事务来检查用户是否已经在锻炼课程中。如果是,那么addReservation()
现在什么都不做。如果他们不是,removeReservation()
也不会做任何事情。
起初我以为我可以禁用按钮并通过逻辑仍然在下面的代码(.disabled()
),但是当我遇到上述竞争条件时,仅此一项不起作用。我发现的是,即使我要添加的对象分别在那里和不在那里,它仍然成功arrayUnion
。arrayRemove
这意味着我的交易可能不会删除reservedUser
不存在的 a 并减少reservationCnt
可能会让我说没有保留用户和reservationCnt
-1
问
有没有更好的方法来处理这个预订过程?我可以在没有交易的情况下完成此操作,至少以某种方式移除用户。理想情况下,我希望在添加或删除用户的预订时让微调器替换按钮,以向用户表明应用正在处理请求。也许我需要两个变量来管理disabled()
状态而不是一个?
MVVM 代码片段
注意:我提取了一些按钮样式以使代码不那么冗长
预订按钮视图
struct ReservationButtonView: View {
var workoutClass: WorkoutClass
@ObservedObject var viewModel: WorkoutClassViewModel
@EnvironmentObject var authViewModel: AuthViewModel
var body: some View {
if checkIsReserved(uid: authViewModel.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
Button(action: {
viewModel.isDisabled = true
viewModel.removeReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Cancel Reservation")
.font(.title) },
icon: { Image(systemName: "person.badge.minus")
.font(.title) }
)
}.disabled(viewModel.isDisabled)
} else{
Button(action: {
viewModel.isDisabled = true
viewModel.addReservation(
documentId: workoutClass.id!,
reservedUserDetails: ["uid": authViewModel.user?.uid as Any, "photoURL": authViewModel.user?.photoURL?.absoluteString ?? "" as Any, "displayName": authViewModel.user?.displayName ?? "Bruin Fitness Member" as Any],
uid: authViewModel.user?.uid ?? "")
}){
Label(
title: { Text("Reserve")
.font(.title) },
icon: { Image(systemName: "person.badge.plus")
.font(.title) }
)
}
.disabled(viewModel.isDisabled)
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
健身课模型
struct reservedUser: Codable, Identifiable {
var id: String = UUID().uuidString
var uid: String
var photoURL: URL?
var displayName: String?
enum CodingKeys: String, CodingKey {
case uid
case photoURL
case displayName
}
}
struct WorkoutClass: Codable,Identifiable {
@DocumentID var id: String?
var reservationCnt: Int
var time: String
var workoutType: String
var reservedUsers: [reservedUser]?
enum CodingKeys: String, CodingKey {
case id
case reservationCnt
case time
case workoutType
case reservedUsers
}
}
WorkoutClassViewModel
class WorkoutClassViewModel: ObservableObject {
@Published var isDisabled = false
private var db = Firestore.firestore()
func addReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let incrementValue: Int64 = 1
let increment = FieldValue.increment(incrementValue)
let addUser = FieldValue.arrayUnion([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve workoutClass from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [])
if isReserved {
print("user is already in class so therefore can't be added again")
return nil
} else {
transaction.updateData(["reservationCnt": increment, "reservedUsers": addUser], forDocument: classReference)
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func removeReservation(documentId: String, reservedUserDetails: [String: Any], uid: String){
let decrementValue: Int64 = -1
let decrement = FieldValue.increment(decrementValue)
let removeUser = FieldValue.arrayRemove([reservedUserDetails])
let classReference = db.document("schedules/Redwood City/dates/\(self.stateDate.dbDateFormat)/classes/\(documentId)")
db.runTransaction { transaction, errorPointer in
let classDocument: DocumentSnapshot
do {
print("Getting classDocument for docId: \(documentId) in addReservedUser()")
try classDocument = transaction.getDocument(classReference)
} catch let fetchError as NSError {
errorPointer?.pointee = fetchError
return nil
}
guard let workoutClass = try? classDocument.data(as: WorkoutClass.self) else {
let error = NSError(
domain: "AppErrorDomain",
code: -3,
userInfo: [
NSLocalizedDescriptionKey: "Unable to retrieve reservedUsers from snapshot \(classDocument)"
]
)
errorPointer?.pointee = error
return nil
}
let isReserved = self.checkIsReserved(uid: uid, reservedUsers: workoutClass.reservedUsers ?? [] )
if isReserved {
transaction.updateData(["reservationCnt": decrement, "reservedUsers": removeUser], forDocument: classReference)
return nil
} else {
print("user not in class so therefore can't be removed")
return nil
}
} completion: { object, error in
if let error = error {
print(error.localizedDescription)
self.isDisabled = false
} else {
print("Successfully ran removeReservation transaction with object: \(object ?? "")")
self.isDisabled = false
}
}
}
func checkIsReserved(uid: String, reservedUsers: [reservedUser]) -> Bool {
return reservedUsers.contains { $0.uid == uid }
}
}
应用截图
预订按钮是视图底部的绿色/灰色按钮
解决方案
我最终通过在决定是否显示“保留”或“取消”按钮的条件之前添加禁用检查条件来解决此问题。
这样,当我的 Firestore 事务正在运行时,用户将看到一个微调器,并且无法对按钮进行猴子测试。微调器有助于显示预订操作正在进行中。当事务达到其完成块时,我禁用isDisabled
Bool 并且侦听器处于同步状态(然后用户会看到新切换的按钮状态)
if workoutClassVM.isDisabled {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: Color("bruinGreenColor")))
} else if checkIsReserved(uid: authVM.user?.uid ?? "", reservedUsers: workoutClass.reservedUsers ?? []) {
...
推荐阅读
- assembly - Nasm - 我如何将变量传递给外部程序?
- timeout - 使用 TALEND 的 EGit 超时
- javascript - Firebase 存储错误 - 不是函数
- html - 多个 HTML 文件中的连续视频壁纸
- java - 关于使用 android studio gradle 3.1.4 和 3.2 构建的确切问题是什么
- java - 方法 orElseThrow(() -> {}) 未为 Page 类型定义
- firebase - 潜在缺陷 - Firebase 在 AAB 测试中的不合理结果
- makefile - Makefile 在另一台机器上抛出缺少分隔符错误
- reactjs - 我想在反应中创建聊天框类型的组件,可以在右下角打开并且可以最小化
- javascript - 将用户输入加载到数组中然后显示