(可选)使用 Firebase Local Emulator Suite 进行原型设计和测试
在介绍应用如何对 Realtime Database 执行读写操作之前,我们先介绍一套可用于对 Realtime Database 功能进行原型设计和测试的工具:Firebase Local Emulator Suite。如果您在尝试使用不同的数据模型、优化安全规则,或设法寻找最经济有效的方式与后端进行交互,那么较为理想的状况是无需实际部署即可在本地进行上述工作。
Realtime Database 模拟器是 Local Emulator Suite 的一部分,通过该模拟器,您的应用可以与模拟的数据库内容和配置进行交互,并可视需要与您的模拟项目资源(函数、其他数据库和安全规则)进行交互。
如需使用 Realtime Database 模拟器,只需完成几个步骤:
- 向应用的测试配置添加一行代码以连接到模拟器。
- 从本地项目的根目录运行
firebase emulators:start
。 - 照常使用 Realtime Database 平台 SDK 或使用 Realtime Database REST API,从应用的原型代码进行调用。
我们提供详细的 Realtime Database 和 Cloud Functions 演示。您还应该参阅 Local Emulator Suite 简介。
获取 FIRDatabaseReference
如需读取或写入数据库数据,您需要一个 FIRDatabaseReference
实例:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
写入数据
本文将介绍读取和写入 Firebase 数据的基础知识。
Firebase 数据会被写入某个 Database
引用,并通过在该引用上附加异步监听器进行检索。该监听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发。
基本写入操作
对于基本写入操作,您可以使用 setValue
将数据保存至指定引用,替换该路径中的任何现有数据。您可以使用此方法执行下列操作:
- 传递与可用 JSON 类型对应的类型(如下所示):
NSString
NSNumber
NSDictionary
NSArray
例如,您可以使用 setValue
添加用户,如下所示:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
以这种方式使用 setValue
将重写指定位置(包括所有子节点)的数据。您也可以在不重写整个对象的情况下更新子节点。如果要允许用户更新其个人资料,您可按照如下所示更新用户名:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
读取数据
通过监听值事件读取数据
如需读取某个路径的数据并监听更改,请使用 FIRDatabaseReference
的 observeEventType:withBlock
观察 FIRDataEventTypeValue
事件。
事件类型 | 典型用法 |
---|---|
FIRDataEventTypeValue |
读取并监听对路径中所有内容的更改。 |
您可以使用 FIRDataEventTypeValue
事件来读取事件发生时给定路径下存在的数据。此方法在附加监听器时触发一次,以后会在每次数据(包括子节点数据)发生更改时再次触发。系统会向事件回调函数传递一个包含该位置中所有数据(包括子节点数据)的 snapshot
。如果该位置没有任何数据,当您调用 exists()
时,快照会返回 false
;当您读取其 value
属性时,快照会返回 nil
。
以下示例演示了社交博客应用如何从数据库中检索博文详细信息:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
监听器接收到一个 FIRDataSnapshot
,其 value
属性中包含事件发生时数据库中指定位置存在的数据。您可以为适当的本机类型赋值,如 NSDictionary
。如果该位置不存在任何数据,则 value
为 nil
。
读取数据一次
使用 getData() 读取一次
此 SDK 旨在管理与数据库服务器的交互,无论您的应用是在线还是离线。
通常,您应该使用上述值事件方法读取数据,以接收后端发出的数据更新的通知。这些方法可减少使用量和费用,并经过优化,可在用户上线和离线时为他们提供最佳体验。
如果您只需要使用数据一次,可以使用 getData()
从数据库获取数据的快照。如果 getData()
由于任何原因无法返回服务器值,客户端将探测本地存储缓存,如果仍找不到该值,则返回错误。
以下示例演示了如何从数据库中检索用户的公开用户名一次:
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-C
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid]; [[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) { if (error) { NSLog(@"Received an error %@", error); return; } NSString *userName = snapshot.value; }];
不必要地使用 getData()
会增加带宽使用量并且会导致性能损失,按照上文所述使用实时监听器可防止这种情况。
使用观察者 (observer) 读取数据一次
在某些情况下,您可能希望立即返回本地缓存中的值,而不必检查服务器上更新后的值。在这些情况下,您可以使用 observeSingleEventOfType
立即从本地磁盘缓存获取数据。
对于只需加载一次且预计不会频繁变化或不需要主动监听的数据,这种方法非常有用。例如,上述示例中的博客应用使用了此方法在用户开始撰写新博文时加载其个人资料:
Swift
let userID = Auth.auth().currentUser?.uid ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in // Get user value let value = snapshot.value as? NSDictionary let username = value?["username"] as? String ?? "" let user = User(username: username) // ... }) { error in print(error.localizedDescription) }
Objective-C
NSString *userID = [FIRAuth auth].currentUser.uid; [[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { // Get user value User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]]; // ... } withCancelBlock:^(NSError * _Nonnull error) { NSLog(@"%@", error.localizedDescription); }];
更新或删除数据
更新特定字段
如需同时向一个节点的某些特定子节点写入数据,而又不重写其他子节点,请使用 updateChildValues
方法。
调用 updateChildValues
时,您可以通过为键指定路径来更新较低层级的子节点值。如果为了更好地实现伸缩而将数据存储在多个位置,可以使用数据扇出更新这些数据的所有实例。例如,社交博客应用可能需要创建一篇博文,同时将其更新到近期活动 Feed 和发布用户的活动 Feed。为此,该博客应用需要使用如下代码:
Swift
guard let key = ref.child("posts").childByAutoId().key else { return } let post = ["uid": userID, "author": username, "title": title, "body": body] let childUpdates = ["/posts/\(key)": post, "/user-posts/\(userID)/\(key)/": post] ref.updateChildValues(childUpdates)
Objective-C
NSString *key = [[_ref child:@"posts"] childByAutoId].key; NSDictionary *post = @{@"uid": userID, @"author": username, @"title": title, @"body": body}; NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post, [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post}; [_ref updateChildValues:childUpdates];
此示例使用 childByAutoId
在包含所有用户博文的节点 (/posts/$postid
) 创建了一篇博文,同时使用 getKey()
检索相应键。接着,该键被用于在用户的博文列表 (/user-posts/$userid/$postid
) 中创建第二个条目。
通过使用这些路径,只需调用 updateChildValues
一次即可同时更新 JSON 树中的多个位置,此示例就使用了这种方式在两个位置同时创建新博文。通过这种方式进行同时更新属于原子操作:所有更新要么全部成功,要么全部失败。
添加完成代码块
若想知道数据是何时提交的,可以添加一个完成代码块。setValue
和 updateChildValues
均支持完成代码块(您可视需要添加),当写入的数据被提交到数据库后,系统就会调用该完成代码块。这个监听器可用于跟踪哪些数据已保存,以及哪些数据仍在同步。如果调用失败,则系统将为该监听器传递一个错误对象,说明失败的原因。
Swift
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Objective-C
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) { if (error) { NSLog(@"Data could not be saved: %@", error); } else { NSLog(@"Data saved successfully."); } }];
删除数据
删除数据最简单的方法是对数据所在位置的引用调用 removeValue
。
您也可以指定 nil
作为另一个写入操作(如 setValue
或 updateChildValues
)的值来进行删除。您可以将此方法与 updateChildValues
结合使用,在一次 API 调用中删除多个子节点。
分离监听器
当您退出 ViewController
时,观察者不会自动停止同步数据。如果未妥善移除,观察者会继续将数据同步到本地内存。当不再需要观察者时,您可以将关联的 FIRDatabaseHandle
传递给 removeObserverWithHandle
方法,以将其移除。
将回调块添加到引用时,系统会返回 FIRDatabaseHandle
。这些句柄可用于移除回调块。
如果有多个监听器添加到了一个数据库引用,则当发生某事件时,系统会调用每一个监听器。如需在该位置停止同步数据,必须通过调用 removeAllObservers
方法来移除其中的所有观察者。
对监听器调用 removeObserverWithHandle
或 removeAllObservers
不会自动移除在子节点上注册的监听器;您还必须跟踪这些引用或句柄才能将其移除。
将数据另存为事务
处理可能因并发修改而损坏的数据(例如,增量计数器)时,您可以使用事务操作。您需要为此操作提供两个参数:更新函数和可选的完成回调函数。更新函数将数据的当前状态视为参数,并返回您要写入的新目标状态。
例如,在示例社交博客应用中,您可以允许用户对博文加星标和移除星标,并跟踪博文获得的星标数,如下所示:
Swift
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in if var post = currentData.value as? [String: AnyObject], let uid = Auth.auth().currentUser?.uid { var stars: [String: Bool] stars = post["stars"] as? [String: Bool] ?? [:] var starCount = post["starCount"] as? Int ?? 0 if let _ = stars[uid] { // Unstar the post and remove self from stars starCount -= 1 stars.removeValue(forKey: uid) } else { // Star the post and add self to stars starCount += 1 stars[uid] = true } post["starCount"] = starCount as AnyObject? post["stars"] = stars as AnyObject? // Set value and report transaction success currentData.value = post return TransactionResult.success(withValue: currentData) } return TransactionResult.success(withValue: currentData) }) { error, committed, snapshot in if let error = error { print(error.localizedDescription) } }
Objective-C
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) { NSMutableDictionary *post = currentData.value; if (!post || [post isEqual:[NSNull null]]) { return [FIRTransactionResult successWithValue:currentData]; } NSMutableDictionary *stars = post[@"stars"]; if (!stars) { stars = [[NSMutableDictionary alloc] initWithCapacity:1]; } NSString *uid = [FIRAuth auth].currentUser.uid; int starCount = [post[@"starCount"] intValue]; if (stars[uid]) { // Unstar the post and remove self from stars starCount--; [stars removeObjectForKey:uid]; } else { // Star the post and add self to stars starCount++; stars[uid] = @YES; } post[@"stars"] = stars; post[@"starCount"] = @(starCount); // Set value and report transaction success currentData.value = post; return [FIRTransactionResult successWithValue:currentData]; } andCompletionBlock:^(NSError * _Nullable error, BOOL committed, FIRDataSnapshot * _Nullable snapshot) { // Transaction completed if (error) { NSLog(@"%@", error.localizedDescription); } }];
如果多个用户同时对同一博文加星或客户端存在过时数据,使用事务可防止加星计数出错。FIRMutableData
类中包含的值最初是客户端最后使用的已知路径值,如果不存在则为 nil
。服务器将初始值与其当前值进行比较,如果这两个值匹配,则接受事务,否则将会拒绝事务。如果事务遭拒绝,则服务器会将当前值返回到客户端,然后客户端使用更新后的值再次运行事务。此过程将反复进行,直到事务被接受或尝试次数达到上限值为止。
原子化服务器端增量
在上述用例中,我们将两个值写入数据库:加星标/移除星标的用户的 ID,以及递增的星标数。如果我们已经知道用户正在为博文加星标,我们可以使用原子化增量操作,而不是事务。
Swift
let updates = [ "posts/\(postID)/stars/\(userID)": true, "posts/\(postID)/starCount": ServerValue.increment(1), "user-posts/\(postID)/stars/\(userID)": true, "user-posts/\(postID)/starCount": ServerValue.increment(1) ] as [String : Any] Database.database().reference().updateChildValues(updates)
Objective-C
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1], [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE, [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]}; [[[FIRDatabase database] reference] updateChildValues:updates];
此代码不使用事务操作,因此它不会在更新有冲突时自动重新运行。但是,由于增量操作直接在数据库服务器上执行,因此不会发生冲突。
如果您希望检测并拒绝应用特有的冲突(例如,用户为之前他们已加星标的博文加星标),那么您应该为该使用场景编写自定义安全规则。
离线处理数据
如果客户端的网络连接中断,您的应用将继续正常运行。
对于所有有效数据,连接到 Firebase 数据库的每个客户端均维护着各自的内部版本。写入数据时,首先会写入这一本地版本。然后,Firebase 客户端会尽可能将这些数据与远程数据库服务器以及其他客户端同步。
因此,对数据库执行的所有写入操作会立即触发本地事件,然后数据才会写入服务器。这意味着应用仍将保持随时响应的状态,无论网络延迟或连接状况如何。
连接重新建立之后,您的应用将收到一系列相应的事件,以便客户端与当前服务器状态进行同步,而不必编写任何自定义代码。
我们将在详细了解在线和离线功能中详细介绍离线行为。