读取和写入数据 (Android)

本文将介绍读取和写入 Firebase 数据的基础知识。

Firebase 数据会被写入某个 FirebaseDatabase 引用,并通过在该引用上附加异步监听器进行检索。该监听器会针对数据的初始状态触发一次,以后只要数据有更改就会再次触发。

(可选)使用 Firebase Local Emulator Suite 进行原型设计和测试

在介绍应用如何对 Realtime Database 执行读写操作之前,我们先介绍一套可用于对 Realtime Database 功能进行原型设计和测试的工具:Firebase Local Emulator Suite。如果您在尝试使用不同的数据模型、优化安全规则,或设法寻找最经济有效的方式与后端进行交互,那么较为理想的状况是无需实际部署即可在本地进行上述工作。

Realtime Database 模拟器是 Local Emulator Suite 的一部分,通过该模拟器,您的应用可以与模拟的数据库内容和配置进行交互,并可视需要与您的模拟项目资源(函数、其他数据库和安全规则)进行交互。

如需使用 Realtime Database 模拟器,只需完成几个步骤:

  1. 向应用的测试配置添加一行代码以连接到模拟器。
  2. 从本地项目的根目录运行 firebase emulators:start
  3. 照常使用 Realtime Database 平台 SDK 或使用 Realtime Database REST API,从应用的原型代码进行调用。

我们提供详细的 Realtime DatabaseCloud Functions 演示。您还应该参阅 Local Emulator Suite 简介

获取 DatabaseReference

如需从数据库读取数据以及将数据写入数据库,您需要一个 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

Listener 事件回调函数 典型用法
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() 方法。当读取操作被取消时,系统会调用该方法。例如,如果客户端没有权限从某一 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;
    }
}

如需创建一篇博文,然后相应地同时更新近期活动 Feed 和用户活动 Feed,该博客应用需使用如下代码:

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() 才能移除回调函数。

以事务方式保存数据

处理可能因并发修改而损坏的数据(例如,增量计数器)时,您可以使用事务操作。您需要为此操作提供两个参数:更新函数和可选的完成回调函数。更新函数接受数据的当前状态作为参数,并返回您要写入的新目标状态。如果另一个客户端在您成功写入新值前向该位置写入数据,则系统会使用新的当前值再次调用更新函数,然后重新尝试执行写入操作。

例如,在示例社交博客应用中,您可以允许用户对博文加星和取消加星,并跟踪博文获得的星数,如下所示:

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() {
        @NonNull
        @Override
        public Transaction.Result doTransaction(@NonNull 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 客户端会在尽力而为的前提下将这些数据与远程数据库服务器以及其他客户端同步。

因此,对数据库执行的所有写入会立即触发本地事件,然后才与服务器进行交互。这意味着无论网络延迟或连接状况如何,应用仍可随时做出响应。

连接重新建立之后,您的应用将收到一系列相应的事件,以便客户端与当前服务器状态进行同步,而不必编写任何自定义代码。

我们将在详细了解在线和离线功能中详细介绍离线行为。

后续步骤