本文档介绍将数据写入 Firebase Realtime Database 的四种方法:设置、更新、推送和事务功能。
保存数据的方法
set | 将数据写入到指定路径(例如 messages/users/<username> )或替换指定路径下的数据 |
update | 更新指定路径中的部分键,而非替换所有数据 |
推送 | 向数据库中的数据列表添加数据。每当您将新节点推送到列表时,您的数据库会生成唯一的键,例如 messages/users/<unique-user-id>/<username> |
事务 | 在处理可能会因并发更新而损坏的复杂数据时,可使用事务 |
保存数据
基本的数据库写入操作是设置,即:将新数据保存到指定的数据库引用,替换该路径的所有现有数据。为帮助您了解设置,我们将构建一个简单的博客应用。应用的数据将存储在以下数据库引用中:
Java
final FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("server/saving-data/fireblog");
Node.js
// Import Admin SDK const { getDatabase } = require('firebase-admin/database'); // Get a database reference to our blog const db = getDatabase(); const ref = db.ref('server/saving-data/fireblog');
Python
# Import database module. from firebase_admin import db # Get a database reference to our blog. ref = db.reference('server/saving-data/fireblog')
Go
// Create a database client from App. client, err := app.Database(ctx) if err != nil { log.Fatalln("Error initializing database client:", err) } // Get a database reference to our blog. ref := client.NewRef("server/saving-data/fireblog")
首先,保存一些用户数据。我们将根据唯一的用户名来存储每位用户,同时还将存储其全名和出生日期。每位用户均具有唯一的用户名,这意味着您已经拥有键,无需另行创建,因此适合使用设置方法(而非推送方法)。
首先,创建一个对您用户数据的数据库引用。然后,使用 set()
/ setValue()
将一个用户对象(其中包含用户的用户名、全名和出生日期)保存到数据库。您可以为设置方法传递字符串、数字、布尔值、null
、数组或任何 JSON 对象。传递 null
将移除指定位置的数据。在本例中,您将向它传递一个对象:
Java
public static class User { public String date_of_birth; public String full_name; public String nickname; public User(String dateOfBirth, String fullName) { // ... } public User(String dateOfBirth, String fullName, String nickname) { // ... } } DatabaseReference usersRef = ref.child("users"); Map<String, User> users = new HashMap<>(); users.put("alanisawesome", new User("June 23, 1912", "Alan Turing")); users.put("gracehop", new User("December 9, 1906", "Grace Hopper")); usersRef.setValueAsync(users);
Node.js
const usersRef = ref.child('users'); usersRef.set({ alanisawesome: { date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }, gracehop: { date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' } });
Python
users_ref = ref.child('users') users_ref.set({ 'alanisawesome': { 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }, 'gracehop': { 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' } })
Go
// User is a json-serializable type. type User struct { DateOfBirth string `json:"date_of_birth,omitempty"` FullName string `json:"full_name,omitempty"` Nickname string `json:"nickname,omitempty"` } usersRef := ref.Child("users") err := usersRef.Set(ctx, map[string]*User{ "alanisawesome": { DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }, "gracehop": { DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }, }) if err != nil { log.Fatalln("Error setting value:", err) }
将 JSON 对象保存到数据库后,系统会自动以嵌套方式将对象属性映射到数据库子位置。现在,如果您前往网址 https://docs-examples.firebaseio.com/server/saving-data/fireblog/users/alanisawesome/full_name,将会看到值“Alan Turing”。您也可以直接将数据保存到某个子位置:
Java
usersRef.child("alanisawesome").setValueAsync(new User("June 23, 1912", "Alan Turing")); usersRef.child("gracehop").setValueAsync(new User("December 9, 1906", "Grace Hopper"));
Node.js
const usersRef = ref.child('users'); usersRef.child('alanisawesome').set({ date_of_birth: 'June 23, 1912', full_name: 'Alan Turing' }); usersRef.child('gracehop').set({ date_of_birth: 'December 9, 1906', full_name: 'Grace Hopper' });
Python
users_ref.child('alanisawesome').set({ 'date_of_birth': 'June 23, 1912', 'full_name': 'Alan Turing' }) users_ref.child('gracehop').set({ 'date_of_birth': 'December 9, 1906', 'full_name': 'Grace Hopper' })
Go
if err := usersRef.Child("alanisawesome").Set(ctx, &User{ DateOfBirth: "June 23, 1912", FullName: "Alan Turing", }); err != nil { log.Fatalln("Error setting value:", err) } if err := usersRef.Child("gracehop").Set(ctx, &User{ DateOfBirth: "December 9, 1906", FullName: "Grace Hopper", }); err != nil { log.Fatalln("Error setting value:", err) }
上面两个例子(将两个值同时写为对象和将它们单独写入子位置)会将相同的数据保存到数据库:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper" } } }
第一个示例仅会在当前正在观察相应数据的客户端上触发一个事件,而第二个示例则会触发两个事件。务必注意,如果 usersRef
已存在数据,则第一种方法会将其覆盖,但是第二种方法只会修改每个单独子节点的值,usersRef
的其他子节点则保持不变。
更新保存的数据
如果要同时写入数据库位置的多个子位置而不覆盖其他子节点,则可使用更新方法,如下所示:
Java
DatabaseReference hopperRef = usersRef.child("gracehop"); Map<String, Object> hopperUpdates = new HashMap<>(); hopperUpdates.put("nickname", "Amazing Grace"); hopperRef.updateChildrenAsync(hopperUpdates);
Node.js
const usersRef = ref.child('users'); const hopperRef = usersRef.child('gracehop'); hopperRef.update({ 'nickname': 'Amazing Grace' });
Python
hopper_ref = users_ref.child('gracehop') hopper_ref.update({ 'nickname': 'Amazing Grace' })
Go
hopperRef := usersRef.Child("gracehop") if err := hopperRef.Update(ctx, map[string]interface{}{ "nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating child:", err) }
这会将 Grace 的数据更新为包含其昵称。如果您在这里使用的是设置方法,而不是个不更新方法,则会从 hopperRef
中同时删除 full_name
和 date_of_birth
。
Firebase Realtime Database 还支持多路径更新。这意味着,更新方法现可同时更新数据库中多个位置的值。该功能十分强大,可以帮助您对数据进行反规范化处理。使用多路径更新可同时为 Grace 和 Alan 添加昵称:
Java
Map<String, Object> userUpdates = new HashMap<>(); userUpdates.put("alanisawesome/nickname", "Alan The Machine"); userUpdates.put("gracehop/nickname", "Amazing Grace"); usersRef.updateChildrenAsync(userUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' });
Python
users_ref.update({ 'alanisawesome/nickname': 'Alan The Machine', 'gracehop/nickname': 'Amazing Grace' })
Go
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome/nickname": "Alan The Machine", "gracehop/nickname": "Amazing Grace", }); err != nil { log.Fatalln("Error updating children:", err) }
执行此更新之后,Alan 和 Grace 均已添加各自的昵称:
{ "users": { "alanisawesome": { "date_of_birth": "June 23, 1912", "full_name": "Alan Turing", "nickname": "Alan The Machine" }, "gracehop": { "date_of_birth": "December 9, 1906", "full_name": "Grace Hopper", "nickname": "Amazing Grace" } } }
请注意,如果您尝试通过写入包含路径的对象来更新对象,将导致不同的行为。我们来看下,如果尝试用这种方法更新 Grace 和 Alan,会发生什么情况:
Java
Map<String, Object> userNicknameUpdates = new HashMap<>(); userNicknameUpdates.put("alanisawesome", new User(null, null, "Alan The Machine")); userNicknameUpdates.put("gracehop", new User(null, null, "Amazing Grace")); usersRef.updateChildrenAsync(userNicknameUpdates);
Node.js
const usersRef = ref.child('users'); usersRef.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } });
Python
users_ref.update({ 'alanisawesome': { 'nickname': 'Alan The Machine' }, 'gracehop': { 'nickname': 'Amazing Grace' } })
Go
if err := usersRef.Update(ctx, map[string]interface{}{ "alanisawesome": &User{Nickname: "Alan The Machine"}, "gracehop": &User{Nickname: "Amazing Grace"}, }); err != nil { log.Fatalln("Error updating children:", err) }
这会导致不同的行为,即覆盖整个 /users
节点:
{ "users": { "alanisawesome": { "nickname": "Alan The Machine" }, "gracehop": { "nickname": "Amazing Grace" } } }
添加完成回调函数
在 Node.js 和 Java Admin SDK 中,如果您想知道数据是何时提交的,可以添加一个完成回调函数。 这些 SDK 中的 set 和 update 方法均支持可选的完成回调函数,当写入的数据被提交到数据库后,系统就会调用该回调函数。如果调用因某种原因而失败,系统将为该回调函数传递一个错误对象,说明失败的原因。 在 Python Admin SDK 和 Go Admin SDK 中,所有写入方法都是阻塞的。也就是说,在写入的内容被提交到数据库之前,写入方法均不会返回。
Java
DatabaseReference dataRef = ref.child("data"); dataRef.setValue("I'm writing data", new DatabaseReference.CompletionListener() { @Override public void onComplete(DatabaseError databaseError, DatabaseReference databaseReference) { if (databaseError != null) { System.out.println("Data could not be saved " + databaseError.getMessage()); } else { System.out.println("Data saved successfully."); } } });
Node.js
dataRef.set('I\'m writing data', (error) => { if (error) { console.log('Data could not be saved.' + error); } else { console.log('Data saved successfully.'); } });
保存数据列表
创建数据列表时,请务必记住大多数应用都具有多用户特性,并相应地调整列表结构。现在我们向您的应用中添加博文,对上面的示例进行扩展。您的第一反应可能是使用设置方法根据自动递增的整数索引来存储子节点,如下所示:
// NOT RECOMMENDED - use push() instead! { "posts": { "0": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "1": { "author": "alanisawesome", "title": "The Turing Machine" } } }
用户添加的新博文会存储为 /posts/2
。这只适用于只有一位作者添加博文的情况。但在协作式博客应用中,许多用户可能会同时添加博文。如果两位作者同时写入 /posts/2
,其中一篇博文将会被另一篇博文删除。
为解决此问题,Firebase 客户端提供了 push()
函数,可为每个新子节点生成一个唯一的键。通过使用唯一的子节点键,多个客户端可以同时向同一位置添加子节点,而不必担心写入冲突。
Java
public static class Post { public String author; public String title; public Post(String author, String title) { // ... } } DatabaseReference postsRef = ref.child("posts"); DatabaseReference newPostRef = postsRef.push(); newPostRef.setValueAsync(new Post("gracehop", "Announcing COBOL, a New Programming Language")); // We can also chain the two calls together postsRef.push().setValueAsync(new Post("alanisawesome", "The Turing Machine"));
Node.js
const newPostRef = postsRef.push(); newPostRef.set({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' }); // we can also chain the two calls together postsRef.push().set({ author: 'alanisawesome', title: 'The Turing Machine' });
Python
posts_ref = ref.child('posts') new_post_ref = posts_ref.push() new_post_ref.set({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' }) # We can also chain the two calls together posts_ref.push().set({ 'author': 'alanisawesome', 'title': 'The Turing Machine' })
Go
// Post is a json-serializable type. type Post struct { Author string `json:"author,omitempty"` Title string `json:"title,omitempty"` } postsRef := ref.Child("posts") newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } if err := newPostRef.Set(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error setting value:", err) } // We can also chain the two calls together if _, err := postsRef.Push(ctx, &Post{ Author: "alanisawesome", Title: "The Turing Machine", }); err != nil { log.Fatalln("Error pushing child node:", err) }
唯一键基于时间戳,因此列表项会自动按时间顺序排列。 因为 Firebase 会为每篇博文生成唯一键,所以即使多位用户同时添加博文,也不会出现写入冲突。您的数据库数据现在如下所示:
{ "posts": { "-JRHTHaIs-jNPLXOQivY": { "author": "gracehop", "title": "Announcing COBOL, a New Programming Language" }, "-JRHTHaKuITFIhnj02kE": { "author": "alanisawesome", "title": "The Turing Machine" } } }
在 JavaScript、Python 和 Go 中,调用 push()
之后立即调用 set()
的模式很常见。因此,您可以使用 Firebase SDK 将要设置的数据直接传递给 push()
,从而结合使用这两种方法,如下所示:
Java
// No Java equivalent
Node.js
// This is equivalent to the calls to push().set(...) above postsRef.push({ author: 'gracehop', title: 'Announcing COBOL, a New Programming Language' });;
Python
# This is equivalent to the calls to push().set(...) above posts_ref.push({ 'author': 'gracehop', 'title': 'Announcing COBOL, a New Programming Language' })
Go
if _, err := postsRef.Push(ctx, &Post{ Author: "gracehop", Title: "Announcing COBOL, a New Programming Language", }); err != nil { log.Fatalln("Error pushing child node:", err) }
获取 push() 生成的唯一键
调用 push()
将返回对新数据路径的引用,您可以使用它来获取键或将数据设置给它。以下代码将生成与上述示例相同的数据,但是现在我们将可访问生成的唯一键:
Java
// Generate a reference to a new location and add some data using push() DatabaseReference pushedPostRef = postsRef.push(); // Get the unique ID generated by a push() String postId = pushedPostRef.getKey();
Node.js
// Generate a reference to a new location and add some data using push() const newPostRef = postsRef.push(); // Get the unique key generated by push() const postId = newPostRef.key;
Python
# Generate a reference to a new location and add some data using push() new_post_ref = posts_ref.push() # Get the unique key generated by push() post_id = new_post_ref.key
Go
// Generate a reference to a new location and add some data using Push() newPostRef, err := postsRef.Push(ctx, nil) if err != nil { log.Fatalln("Error pushing child node:", err) } // Get the unique key generated by Push() postID := newPostRef.Key
如您所见,您可以从 push()
引用中获取唯一键的值。
在下文的检索数据部分中,我们将了解如何从 Firebase 数据库读取这些数据。
保存事务数据
在处理可能会因并发修改而损坏的复杂数据(例如增量计数器)时,SDK 为您提供了事务处理操作。
在 Java 和 Node.js 中,您可为事务处理操作提供两个回调函数:更新函数和可选的完成回调函数。在 Python 和 Go 中,事务处理操作是阻塞型的,因此它只接受更新函数。
更新函数将数据的当前状态作为参数传入,并应返回您要写入的新目标状态。例如,如果要增加特定博文的点赞数,可以编写如下事务:
Java
DatabaseReference upvotesRef = ref.child("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes"); upvotesRef.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { Integer currentValue = mutableData.getValue(Integer.class); if (currentValue == null) { mutableData.setValue(1); } else { mutableData.setValue(currentValue + 1); } return Transaction.success(mutableData); } @Override public void onComplete( DatabaseError databaseError, boolean committed, DataSnapshot dataSnapshot) { System.out.println("Transaction completed"); } });
Node.js
const upvotesRef = db.ref('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes'); upvotesRef.transaction((current_value) => { return (current_value || 0) + 1; });
Python
def increment_votes(current_value): return current_value + 1 if current_value else 1 upvotes_ref = db.reference('server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes') try: new_vote_count = upvotes_ref.transaction(increment_votes) print('Transaction completed') except db.TransactionAbortedError: print('Transaction failed to commit')
Go
fn := func(t db.TransactionNode) (interface{}, error) { var currentValue int if err := t.Unmarshal(¤tValue); err != nil { return nil, err } return currentValue + 1, nil } ref := client.NewRef("server/saving-data/fireblog/posts/-JRHTHaIs-jNPLXOQivY/upvotes") if err := ref.Transaction(ctx, fn); err != nil { log.Fatalln("Transaction failed to commit:", err) }
上述示例会检查计数器是否是 null
(即从未增加过),因为客户端可能会在未写入默认值的情况下使用 null
调用事务。
如果上述代码在没有事务处理函数的情况下运行,并且两个客户端同时尝试增加计数器的值,则这两个客户端均会将 1
作为新值写入,从而产生一个增量(而不是两个)。
网络连接和离线写入
Firebase 的 Node.js 和 Java 客户端都会各自维护任何活动数据的内部版本。写入数据时,首先将其写入此本地版本。然后,客户端尽最大程度将这些数据与数据库以及其他客户端同步。
因此,对数据库执行的所有写入操作会立即触发本地事件,然后数据才会写入服务器。这意味着,如果您的应用是使用 Firebase 编写的,那么即使遇到网络延迟或互联网连接问题,您的应用仍可做出响应。
重新建立连接之后,我们将收到一系列相应的事件,以便客户端能与当前服务器状态同步,而不必编写任何自定义代码。
保护数据
Firebase Realtime Database 采用了一种安全语言,您可以通过这种语言定义哪些用户拥有对您数据的不同节点的读取和写入权限。如需了解详情,请参阅保护数据。