在 Android 上讀取及寫入資料

本文說明讀取及寫入 Firebase 資料的基本概念。

Firebase 資料會寫入 FirebaseDatabase 參照,並由 將非同步事件監聽器附加到參照中。事件監聽器會觸發 也就是資料的初始狀態,並在資料變更時再次執行

(選用) 使用 Firebase 本機模擬器套件設計原型並進行測試

在討論應用程式如何讀取即時資料庫及寫入資料之前, 現在就來介紹一組工具 功能:Firebase 本機模擬器套件。如要透過其他資料 最佳化安全性規則,或是盡量找出 以符合成本效益的方式與後端互動 未部署即時服務,也是不錯的點子

即時資料庫模擬器屬於本機模擬器套件的一部分 可讓應用程式與模擬的資料庫內容和設定互動,如 以及選用的模擬專案資源 (函式、其他資料庫 和安全性規則)。

使用即時資料庫模擬器只需完成幾個步驟:

  1. 將一行程式碼新增至應用程式的測試設定,即可與模擬器連線。
  2. 從本機專案目錄的根目錄中執行 firebase emulators:start
  3. 使用即時資料庫平台,從應用程式的原型程式碼發出呼叫 繼續使用 SDK,或是使用即時資料庫 REST API。

歡迎查看詳細的即時資料庫和 Cloud Functions 逐步操作說明。建議您也參閱「本機模擬器套件簡介」一文。

取得 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

監聽器 事件回呼 一般用量
ValueEventListener onDataChange() 讀取及監聽路徑完整內容的異動。

您可以使用 onDataChange() 方法讀取 的內容就存在於事件發生時,就存在於指定路徑上。這個方法 系統會在您附加監聽器時觸發一次,而每當資料 包括子項、變更事件回呼會傳遞一個快照,其中包含 該位置的所有資料,包括兒童資料。如果沒有資料, 呼叫 exists()null 時,快照將傳回 false 還有 getValue()

下列範例示範社交網誌應用程式擷取 資料庫中貼文的詳細資料:

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() 時,您可以透過 指定金鑰的路徑如果資料儲存在多個位置,以便進行擴充 您可以使用 kubectl 指令 資料擴散傳遞。舉例來說 社群網誌應用程式可能有一個 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 呼叫中刪除多個子項。

卸離事件監聽器

呼叫 removeEventListener() 方法,即可移除回呼 Firebase 資料庫參考資料。

如果已在某個資料位置中多次新增監聽器, 呼叫 以便多次呼叫 呼叫,您必須解除 時間進行完全移除

對父項事件監聽器呼叫 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() {
        @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);
        }
    });
}

使用交易時,如果多個 使用者同時對同一則貼文加上星號,或客戶收集到過時資料。如果 交易遭拒,伺服器會將目前的值傳回用戶端 這會使用更新後的值再次執行交易。重複執行直到 交易或進行次數過多

整體伺服器端增量

在上述用途中,我們要將兩個值寫入資料庫: 使用者為貼文加上星號/移除星號,以及逐漸增加的星號數量。如果我們 就可以知道使用者已為貼文加上星號 而不是交易作業

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 用戶端會將這些資料與 協助遠端資料庫伺服器,並與其他用戶端「盡力」。

因此,所有寫入資料庫的動作都會立即觸發本機事件, 所有與伺服器的互動這表示您的應用程式仍可正常回應 可以限制 Cloud Storage 服務

連線恢復後,應用程式會收到一組適當的 以便用戶端與目前的伺服器狀態同步, 即可撰寫任何自訂程式碼

我們將在下列單元中進一步說明離線行為: 進一步瞭解線上和離線功能

後續步驟