全新推出 Cloud Firestore(测试版):试用 Firebase 和 Google Cloud Platform 全新推出的规模可扩展且灵活的数据库。详细了解 Cloud Firestore

在 Android 上读取和写入数据

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

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

获取 DatabaseReference

要从数据库读取数据或将数据写入到数据库中,您需要一个 DatabaseReference 实例:

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

读取和写入数据

基本写入操作

对于基本写入操作,您可以使用 setValue() 将数据保存至指定引用,替换该路径的任何现有数据。您可以使用此方法执行下列操作:

  • 传递与可用 JSON 类型对应的类型(如下所示):
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • 传递自定义 Java 对象(如果定义该对象的类的默认构造函数不接受参数,且该类为要分配的属性提供了公共 getter 方法)。

如果使用 Java 对象,则对象的内容将自动以嵌套方式映射到子位置。使用 Java 对象通常还会提高代码的可读性,使其更易于维护。例如,如果应用包含用户的基本个人资料,则 User 对象可能如下所示:

@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() 添加用户,如下所示:

private void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

以这种方式使用 setValue() 将重写指定位置的数据,包括所有子节点。但是,您仍可在不重写整个对象的情况下更新子节点。如果要允许用户更新其个人资料,您可按照如下所示更新用户名:

mDatabase.child("users").child(userId).child("username").setValue(name);

侦听值事件

要读取路径中的数据并侦听更改,您可使用 addValueEventListener()addListenerForSingleValueEvent() 方法,将 ValueEventListener 添加到 DatabaseReference

侦听器 事件回调函数 典型用法
ValueEventListener onDataChange() 读取并侦听对路径中所有内容的更改。

您可以使用 onDataChange() 方法读取事件发生时存在于给定路径中的内容的静态快照。此方法在附加侦听器时触发一次,以后会在每次数据(包括子节点数据)发生更改时再次触发。系统会向事件回调函数传递一个包含该位置中所有数据(包括子节点数据)的快照。如果该位置没有任何数据,则返回的快照为 null

以下示例演示了社交博客应用如何从数据库中检索博文详细信息:

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 对象,说明失败的原因。

读取数据一次

在某些情况下,您需要在调用一次回调函数后立即将其移除(例如,在初始化预计不会发生更改的界面元素时)。您可以使用 addListenerForSingleValueEvent() 方法简化这种情况:仅触发回调函数一次,以后不会再次触发。

对于只需加载一次且预计不会频繁变化或不需要主动侦听的数据,这种方法非常有用。例如,上述示例中的博客应用使用了此方法在用户开始撰写新博文时加载其个人资料:

更新或删除数据

更新特定字段

要同时向一个节点的特定子节点写入数据,而不重写其他子节点,请使用 updateChildren() 方法。

调用 updateChildren() 时,可以通过为键指定路径来更新较低层级的子节点值。如果为了更好地扩展规模而将数据存储在多个位置,则可使用数据扇出更新这些数据的所有副本。例如,社交博客应用可能具有一个 Post 类,如下所示:

@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,该博客应用需使用如下代码:

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() 均支持完成侦听器(您可视需要添加),当写入的数据被提交到数据库后,系统就会调用该完成侦听器。如果调用失败,则系统将为该侦听器传递一个错误对象,说明失败的原因。

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

将数据另存为事务

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

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

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 b,
                               DataSnapshot dataSnapshot) {
            // Transaction completed
            Log.d(TAG, "postTransaction:onComplete:" + databaseError);
        }
    });
}

如果多个用户同时对同一博文加星或客户端存在过时数据,使用事务可防止加星计数出错。如果事务遭拒绝,则服务器会将当前值返回到客户端,然后客户端使用更新后的值再次运行事务。此过程将反复进行,直到事务被接受或尝试次数达到限制为止。

离线写入数据

如果客户端的网络连接中断,您的应用将继续正常运行。

对于所有有效数据,连接到 Firebase 数据库的每个客户端均维护着各自的内部版本。数据在写入时,首先会写入这类本地版本。然后,Firebase 客户端会尽可能将这些数据与远程数据库服务器以及其他客户端同步。

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

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

后续步骤

发送以下问题的反馈:

此网页
Firebase 实时数据库
需要帮助?请访问我们的支持页面