(可选)使用 Firebase Local Emulator Suite 制作原型并进行测试
在讨论您的应用程序如何读取和写入实时数据库之前,让我们介绍一组可用于原型设计和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型、优化您的安全规则,或者努力寻找与后端交互的最具成本效益的方式,那么能够在不部署实时服务的情况下在本地工作可能是一个好主意。
Realtime Database 模拟器是 Local Emulator Suite 的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及可选的模拟项目资源(函数、其他数据库和安全规则)进行交互。
使用实时数据库模拟器只需几个步骤:
- 在您的应用程序的测试配置中添加一行代码以连接到模拟器。
- 从本地项目目录的根目录运行
firebase emulators:start
。 - 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。
提供了涉及实时数据库和云函数的详细演练。您还应该看看Local Emulator Suite introduction 。
获取 FIRDatabaseReference
要从数据库读取或写入数据,您需要一个FIRDatabaseReference
实例:
迅速
var ref: DatabaseReference! ref = Database.database().reference()
目标-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
写数据
本文档介绍了读取和写入 Firebase 数据的基础知识。
Firebase 数据被写入Database
引用,并通过将异步侦听器附加到引用来检索。侦听器针对数据的初始状态触发一次,并在数据更改时再次触发。
基本写操作
对于基本的写入操作,您可以使用setValue
将数据保存到指定的引用,替换该路径中的任何现有数据。您可以使用此方法来:
- 传递对应于可用 JSON 类型的类型,如下所示:
-
NSString
-
NSNumber
-
NSDictionary
-
NSArray
-
例如,您可以使用setValue
添加用户,如下所示:
迅速
self.ref.child("users").child(user.uid).setValue(["username": username])
目标-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
以这种方式使用setValue
会覆盖指定位置的数据,包括任何子节点。但是,您仍然可以在不重写整个对象的情况下更新子对象。如果你想让用户更新他们的个人资料,你可以按如下方式更新用户名:
迅速
self.ref.child("users/\(user.uid)/username").setValue(username)
目标-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
读取数据
通过监听值事件读取数据
要读取路径上的数据并监听变化,请使用 FIRDatabaseReference 的observeEventType:withBlock
FIRDatabaseReference
来观察FIRDataEventTypeValue
事件。
事件类型 | 典型用法 |
---|---|
FIRDataEventTypeValue | 读取并监听路径全部内容的变化。 |
您可以使用FIRDataEventTypeValue
事件读取给定路径中的数据,因为它在事件发生时存在。当附加侦听器时会触发一次此方法,每次数据(包括任何子项)更改时都会再次触发。向事件回调传递一个snapshot
,其中包含该位置的所有数据,包括子数据。如果没有数据,快照将在您调用exists()
时返回false
,在您读取其value
属性时返回nil
。
以下示例演示了一个社交博客应用程序从数据库中检索帖子的详细信息:
迅速
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
目标-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
侦听器收到一个FIRDataSnapshot
,其中包含事件发生时数据库中指定位置的数据,其value
属性为该数据。您可以将值分配给适当的本机类型,例如NSDictionary
。如果该位置不存在数据,则value
nil
。
一次读取数据
使用 getData() 读取一次
SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。
通常,您应该使用上述值事件技术来读取数据,以便从后端获得数据更新的通知。这些技术可以减少您的使用和计费,并且经过优化可以为您的用户提供在线和离线时的最佳体验。
如果只需要一次数据,可以使用getData()
从数据库中获取数据的快照。如果出于任何原因getData()
无法返回服务器值,客户端将探测本地存储缓存并在仍未找到该值时返回错误。
以下示例演示了从数据库中一次性检索用户的面向公众的用户名:
迅速
ref.child("users/\(uid)/username").getData(completion: { error, snapshot in guard error == nil else { print(error!.localizedDescription) return; } let userName = snapshot.value as? String ?? "Unknown"; });
目标-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()
会增加带宽的使用并导致性能损失,这可以通过使用如上所示的实时侦听器来防止。
用观察者读取一次数据
在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用observeSingleEventOfType
立即从本地磁盘缓存中获取数据。
这对于只需要加载一次并且预计不会频繁更改或不需要主动侦听的数据很有用。例如,前面示例中的博客应用程序使用此方法在用户开始创作新帖子时加载用户的个人资料:
迅速
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) }
目标-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
时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用数据扇出更新该数据的所有实例。例如,一个社交博客应用程序可能想要创建一个帖子并同时将其更新为最近的活动提要和发帖用户的活动提要。为此,博客应用程序使用如下代码:
迅速
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)
目标-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
都采用一个可选的完成块,当写入已提交到数据库时调用该块。此侦听器可用于跟踪哪些数据已保存以及哪些数据仍在同步。如果调用不成功,则会向侦听器传递一个错误对象,指示失败发生的原因。
迅速
ref.child("users").child(user.uid).setValue(["username": username]) { (error:Error?, ref:DatabaseReference) in if let error = error { print("Data could not be saved: \(error).") } else { print("Data saved successfully!") } }
目标-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
不会自动删除在其子节点上注册的侦听器;您还必须跟踪这些引用或句柄以将其删除。
将数据保存为事务
当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。你给这个操作两个参数:一个更新函数和一个可选的完成回调。 update 函数将数据的当前状态作为参数并返回您想要写入的新的所需状态。
例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子收到的星标数量,如下所示:
迅速
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) } }
目标-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,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。
迅速
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);
目标-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 客户端会在“尽力”的基础上将该数据与远程数据库服务器和其他客户端同步。
因此,在将任何数据写入服务器之前,所有对数据库的写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用程序都会保持响应。
重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。
我们将在了解有关在线和离线功能的更多信息中详细讨论离线行为。