本文档介绍了读取和写入 Firebase 数据的基础知识。
Firebase 数据写入FirebaseDatabase
引用,并通过将异步侦听器附加到引用来检索。侦听器针对数据的初始状态触发一次,并在数据更改时再次触发。
(可选)使用 Firebase Local Emulator Suite 制作原型并进行测试
在讨论您的应用程序如何读取和写入实时数据库之前,让我们介绍一组可用于原型设计和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型、优化您的安全规则,或者努力寻找与后端交互的最具成本效益的方式,那么能够在不部署实时服务的情况下在本地工作可能是一个好主意。
Realtime Database 模拟器是 Local Emulator Suite 的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及可选的模拟项目资源(函数、其他数据库和安全规则)进行交互。
使用实时数据库模拟器只需几个步骤:
- 在您的应用程序的测试配置中添加一行代码以连接到模拟器。
- 从本地项目目录的根目录运行
firebase emulators:start
。 - 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。
提供了涉及实时数据库和云函数的详细演练。您还应该看看Local Emulator Suite introduction 。
获取数据库引用
要从数据库读取或写入数据,您需要一个DatabaseReference
实例:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
写数据
基本写操作
对于基本的写入操作,您可以使用setValue()
将数据保存到指定的引用,替换该路径中的任何现有数据。您可以使用此方法来:
- 传递对应于可用 JSON 类型的类型,如下所示:
-
String
-
Long
-
Double
-
Boolean
-
Map<String, Object>
-
List<Object>
-
- 传递自定义 Java 对象,如果定义它的类具有默认构造函数,该构造函数不接受参数并且具有用于要分配的属性的公共 getter。
如果您使用 Java 对象,您的对象的内容将以嵌套方式自动映射到子位置。使用 Java 对象通常还可以使您的代码更具可读性和更易于维护。例如,如果您有一个带有基本用户配置文件的应用程序,您的User
对象可能如下所示:
Kotlin+KTX
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
您可以使用setValue()
添加用户,如下所示:
Kotlin+KTX
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
以这种方式使用setValue()
会覆盖指定位置的数据,包括任何子节点。但是,您仍然可以在不重写整个对象的情况下更新子对象。如果你想让用户更新他们的个人资料,你可以按如下方式更新用户名:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
读取数据
使用持久性侦听器读取数据
要读取路径中的数据并侦听更改,请使用addValueEventListener()
方法将ValueEventListener
添加到DatabaseReference
。
聆听者 | 事件回调 | 典型用法 |
---|---|---|
ValueEventListener | onDataChange() | 读取并监听路径全部内容的变化。 |
您可以使用onDataChange()
方法读取给定路径中内容的静态快照,因为它们在事件发生时就已存在。当监听器被附加时,这个方法被触发一次,每次数据(包括孩子)发生变化时再次触发。向事件回调传递一个快照,其中包含该位置的所有数据,包括子数据。如果没有数据,调用exists()
时快照将返回false
,调用getValue()
时返回null
。
以下示例演示了一个社交博客应用程序从数据库中检索帖子的详细信息:
Kotlin+KTX
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
侦听器收到一个DataSnapshot
,其中包含事件发生时数据库中指定位置的数据。在快照上调用getValue()
会返回数据的 Java 对象表示。如果该位置不存在数据,则调用getValue()
将返回null
。
在此示例中, ValueEventListener
还定义了在取消读取时调用的 onCancelled onCancelled()
方法。例如,如果客户端没有从 Firebase 数据库位置读取的权限,则可以取消读取。向此方法传递一个DatabaseError
对象,指示发生故障的原因。
一次读取数据
使用 get() 读取一次
SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。
通常,您应该使用上述的ValueEventListener
技术来读取数据,以便从后端获得数据更新的通知。侦听器技术可减少您的使用和计费,并经过优化以在用户在线和离线时为他们提供最佳体验。
如果只需要一次数据,可以使用get()
从数据库中获取数据的快照。如果出于任何原因get()
无法返回服务器值,客户端将探测本地存储缓存并在仍未找到该值时返回错误。
不必要地使用get()
会增加带宽的使用并导致性能损失,这可以通过使用如上所示的实时侦听器来防止。
Kotlin+KTX
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
使用监听器读取一次
在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用addListenerForSingleValueEvent
立即从本地磁盘缓存中获取数据。
这对于只需要加载一次并且预计不会频繁更改或不需要主动侦听的数据很有用。例如,前面示例中的博客应用程序使用此方法在用户开始创作新帖子时加载用户的个人资料。
更新或删除数据
更新特定字段
要同时写入一个节点的特定子节点而不覆盖其他子节点,请使用updateChildren()
方法。
调用updateChildren()
时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用数据扇出更新该数据的所有实例。例如,一个社交博客应用程序可能有一个像这样的Post
类:
Kotlin+KTX
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap() ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
要创建帖子并同时将其更新为最近的活动提要和发帖用户的活动提要,博客应用程序使用如下代码:
Kotlin+KTX
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
此示例使用push()
在包含所有用户在/posts/$postid
的帖子的节点中创建帖子,并同时使用getKey()
检索密钥。然后可以使用该密钥在/user-posts/$userid/$postid
的用户帖子中创建第二个条目。
使用这些路径,您可以通过一次调用updateChildren()
对 JSON 树中的多个位置执行同时更新,例如本示例如何在两个位置创建新帖子。以这种方式进行的同时更新是原子的:要么所有更新都成功,要么所有更新都失败。
添加完成回调
如果您想知道您的数据何时提交,您可以添加一个完成侦听器。 setValue()
和updateChildren()
一个可选的完成侦听器,当写入成功提交到数据库时调用该侦听器。如果调用不成功,则会向侦听器传递一个错误对象,指示失败发生的原因。
Kotlin+KTX
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
删除数据
删除数据的最简单方法是在对该数据位置的引用上调用removeValue()
。
您还可以通过将null
指定为另一个写入操作(例如setValue()
或updateChildren()
的值来删除。您可以将此技术与updateChildren()
结合使用,以在单个 API 调用中删除多个子项。
分离监听器
通过对 Firebase 数据库引用调用removeEventListener()
方法来删除回调。
如果一个侦听器已多次添加到数据位置,则每个事件都会多次调用它,并且您必须将其分离相同的次数才能将其完全删除。
在父监听器上调用removeEventListener()
不会自动删除在其子节点上注册的监听器;还必须对任何子侦听器调用removeEventListener()
以删除回调。
将数据保存为事务
当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。你给这个操作两个参数:一个更新函数和一个可选的完成回调。 update 函数将数据的当前状态作为参数并返回您想要写入的新的期望状态。如果另一个客户端在您的新值成功写入之前写入该位置,则会使用新的当前值再次调用您的更新函数,然后重试写入。
例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子收到的星标数量,如下所示:
Kotlin+KTX
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot? ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @Override public Transaction.Result doTransaction(MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
如果多个用户同时为同一个帖子加注星标或者客户的数据过时,使用事务可以防止加星计数不正确。如果事务被拒绝,服务器将当前值返回给客户端,客户端使用更新后的值再次运行事务。重复此过程,直到交易被接受或尝试次数过多。
原子服务器端增量
在上面的用例中,我们向数据库写入了两个值:对帖子加星/取消加星的用户 ID,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。
Kotlin+KTX
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1) ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(updates); }
此代码不使用事务操作,因此如果存在更新冲突,它不会自动重新运行。但是,由于增量操作直接发生在数据库服务器上,因此不会发生冲突。
如果您想检测并拒绝特定于应用程序的冲突,例如用户为他们之前已经加星标的帖子加注星标,您应该为该用例编写自定义安全规则。
离线处理数据
如果客户端失去网络连接,您的应用程序将继续正常运行。
连接到 Firebase 数据库的每个客户端都维护自己的内部版本的任何数据,这些数据正在使用侦听器或标记为与服务器保持同步。读取或写入数据时,首先使用该数据的本地版本。然后,Firebase 客户端会在“尽力”的基础上将该数据与远程数据库服务器和其他客户端同步。
因此,在与服务器进行任何交互之前,所有对数据库的写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用程序都会保持响应。
重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。
我们将在了解有关在线和离线功能的更多信息中详细讨论离线行为。