在 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 平台從應用程式的原型程式碼發出呼叫 或 Realtime Database REST API 繼續使用 SDK。

請參閱有關 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

監聽器 事件回呼 一般用量
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;
    }
}

如要建立貼文並同時將其更新至最近活動動態消息和發布者使用者的活動動態消息,網誌應用程式會使用以下程式碼:

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() {
        @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 服務

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

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

後續步驟