获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

在 Android 上读取和写入数据

本文档涵盖了读取和写入 Firebase 数据的基础知识。

Firebase 数据被写入FirebaseDatabase引用,并通过将异步侦听器附加到引用来检索。侦听器会针对数据的初始状态触发一次,并在数据更改时再次触发。

(可选)使用 Firebase 本地模拟器套件进行原型设计和测试

在讨论您的应用如何读取和写入实时数据库之前,让我们介绍一组可用于原型化和测试实时数据库功能的工具:Firebase Local Emulator Suite。如果您正在尝试不同的数据模型,优化您的安全规则,或努力寻找与后端交互的最具成本效益的方式,那么无需部署实时服务即可在本地工作可能是一个好主意。

实时数据库模拟器是本地模拟器套件的一部分,它使您的应用程序能够与您的模拟数据库内容和配置以及您的模拟项目资源(函数、其他数据库和安全规则)进行交互。

使用实时数据库模拟器只需几个步骤:

  1. 在应用程序的测试配置中添加一行代码以连接到模拟器。
  2. 从本地项目目录的根目录运行firebase emulators:start
  3. 像往常一样使用实时数据库平台 SDK 或使用实时数据库 REST API 从应用程序的原型代码进行调用。

提供了涉及实时数据库和云函数的详细演练。您还应该查看Local Emulator Suite 介绍

获取数据库引用

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

Java

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

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

写入数据

基本写操作

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

  • 传递与可用 JSON 类型相对应的类型,如下所示:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • 如果定义它的类具有不带参数的默认构造函数并且具有用于分配属性的公共 getter,则传递自定义 Java 对象。

如果使用 Java 对象,则对象的内容会自动以嵌套方式映射到子位置。使用 Java 对象通常还可以使您的代码更具可读性和更易于维护。例如,如果您有一个具有基本用户配置文件的应用程序,您的User对象可能如下所示:

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;
    }

}

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.
}

您可以使用setValue()添加用户,如下所示:

Java

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

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

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

以这种方式使用setValue()会覆盖指定位置的数据,包括任何子节点。但是,您仍然可以在不重写整个对象的情况下更新子对象。如果您想允许用户更新他们的个人资料,您可以按如下方式更新用户名:

Java

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

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

读取数据

使用持久监听器读取数据

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

听众事件回调典型用法
ValueEventListener onDataChange()阅读并监听路径全部内容的变化。

您可以使用onDataChange()方法读取给定路径中内容的静态快照,因为它们在事件发生时就存在。此方法在附加侦听器时触发一次,并且在每次数据(包括子项)发生更改时再次触发。事件回调被传递一个快照,其中包含该位置的所有数据,包括子数据。如果没有数据,则调用exists()时快照将返回false ,调用getValue()时返回null

以下示例演示了一个社交博客应用程序从数据库中检索帖子的详细信息:

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);

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)

侦听器收到一个DataSnapshot ,其中包含事件发生时数据库中指定位置的数据。对快照调用getValue()会返回数据的 Java 对象表示。如果该位置不存在数据,则调用getValue()将返回null

在此示例中, ValueEventListener还定义了在取消读取时调用的 onCancelled onCancelled()方法。例如,如果客户端无权从 Firebase 数据库位置读取,则可以取消读取。此方法传递一个DatabaseError对象,指示失败发生的原因。

读取数据一次

使用 get() 读取一次

SDK 旨在管理与数据库服务器的交互,无论您的应用程序是在线还是离线。

通常,您应该使用上述ValueEventListener技术来读取数据,以便从后端获取数据更新的通知。侦听器技术可减少您的使用量和计费,并经过优化以在您的用户在线和离线时为他们提供最佳体验。

如果您只需要一次数据,您可以使用get()从数据库中获取数据的快照。如果由于任何原因get()无法返回服务器值,客户端将探测本地存储缓存,如果仍未找到该值,则返回错误。

不必要地使用get()会增加带宽的使用并导致性能损失,这可以通过使用实时侦听器来防止,如上所示。

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()));
        }
    }
});

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

使用监听器读取一次

在某些情况下,您可能希望立即返回本地缓存中的值,而不是检查服务器上的更新值。在这些情况下,您可以使用addListenerForSingleValueEvent立即从本地磁盘缓存中获取数据。

这对于只需要加载一次并且预计不会频繁更改或需要主动侦听的数据很有用。例如,前面示例中的博客应用程序使用此方法在用户开始创作新帖子时加载他们的个人资料。

更新或删除数据

更新特定字段

要同时写入节点的特定子节点而不覆盖其他子节点,请使用updateChildren()方法。

调用updateChildren()时,您可以通过指定键的路径来更新较低级别的子值。如果数据存储在多个位置以更好地扩展,您可以使用data fan-out更新该数据的所有实例。例如,一个社交博客应用程序可能有一个Post类,如下所示:

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

@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

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);
}

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)
}

此示例使用push()在包含/posts/$postid的所有用户的帖子的节点中创建帖子,并同时使用getKey()检索密钥。然后可以使用该密钥在/user-posts/$userid/$postid的用户帖子中创建第二个条目。

使用这些路径,您可以通过一次调用updateChildren()来同时更新 JSON 树中的多个位置,例如此示例如何在两个位置创建新帖子。以这种方式进行的同时更新是原子的:要么所有更新都成功,要么所有更新都失败。

添加完成回调

如果您想知道您的数据何时提交,您可以添加一个完成侦听器。 setValue()updateChildren()都采用一个可选的完成侦听器,当写入成功提交到数据库时调用该侦听器。如果调用不成功,则向侦听器传递一个指示失败原因的错误对象。

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
                // ...
            }
        });

Kotlin+KTX

database.child("users").child(userId).setValue(user)
        .addOnSuccessListener {
            // Write was successful!
            // ...
        }
        .addOnFailureListener {
            // Write failed
            // ...
        }

删除数据

删除数据的最简单方法是在对该数据位置的引用上调用removeValue()

您还可以通过将null指定为另一个写入操作(例如setValue()updateChildren()的值来删除。您可以将此技术与updateChildren()一起使用,以在单个 API 调用中删除多个子项。

分离监听器

通过在 Firebase 数据库引用上调用removeEventListener()方法来删​​除回调。

如果已将侦听器多次添加到数据位置,则每个事件都会多次调用它,并且您必须将其分离相同的次数才能将其完全删除。

在父侦听器上调用removeEventListener()不会自动删除在其子节点上注册的侦听器;还必须在任何子侦听器上调用removeEventListener()以删除回调。

将数据保存为事务

当处理可能被并发修改破坏的数据时,例如增量计数器,您可以使用事务操作。你给这个操作两个参数:一个更新函数和一个可选的完成回调。更新函数将数据的当前状态作为参数,并返回您想要写入的新状态。如果另一个客户端在您的新值成功写入之前写入该位置,则使用新的当前值再次调用您的更新函数,并重试写入。

例如,在示例社交博客应用程序中,您可以允许用户为帖子加注星标和取消加注星标,并跟踪帖子获得的星数,如下所示:

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);
        }
    });
}

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!!)
        }
    })
}

如果多个用户同时为同一个帖子加注星标或客户端的数据过时,使用事务可以防止星标计数不正确。如果事务被拒绝,服务器将当前值返回给客户端,客户端使用更新后的值再次运行事务。这会重复,直到交易被接受或进行了太多尝试。

原子服务器端增量

在上面的用例中,我们将两个值写入数据库:为帖子加星/取消星标的用户的 ID,以及增加的星数。如果我们已经知道用户正在为帖子加注星标,我们可以使用原子增量操作而不是事务。

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);
}

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)
}

此代码不使用事务操作,因此如果存在冲突更新,它不会自动重新运行。但是,由于增量操作直接发生在数据库服务器上,因此不会发生冲突。

如果您想检测和拒绝特定于应用程序的冲突,例如用户为他们之前已加注星标的帖子加注星标,您应该为该用例编写自定义安全规则。

离线处理数据

如果客户端失去其网络连接,您的应用程序将继续正常运行。

每个连接到 Firebase 数据库的客户端都维护自己的内部版本,其中包含正在使用侦听器或标记为与服务器保持同步的任何数据。读取或写入数据时,首先使用该数据的本地版本。然后,Firebase 客户端会在“尽力而为”的基础上将该数据与远程数据库服务器和其他客户端同步。

因此,在与服务器进行任何交互之前,对数据库的所有写入都会立即触发本地事件。这意味着无论网络延迟或连接如何,您的应用都会保持响应。

重新建立连接后,您的应用程序会收到一组适当的事件,以便客户端与当前服务器状态同步,而无需编写任何自定义代码。

我们将在了解有关在线和离线功能的更多信息中详细讨论离线行为。

下一步