In diesem Dokument werden die Grundlagen zum Lesen und Schreiben von Firebase-Daten behandelt.
Firebase-Daten werden in eine FirebaseDatabase-Referenz geschrieben und abgerufen, indem ein asynchroner Listener an die Referenz angehängt wird. Der Listener wird einmal für den anfänglichen Status der Daten und dann bei jeder Änderung der Daten ausgelöst.
(Optional) Prototypen erstellen und mit Firebase Local Emulator Suite testen
Bevor wir darauf eingehen, wie Ihre App Daten aus Realtime Databaseliest und in sie schreibt, stellen wir Ihnen eine Reihe von Tools vor, mit denen Sie Prototypen erstellen und die Realtime Database Funktionalität testen können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder nach der kostengünstigsten Möglichkeit suchen, mit dem Backend zu interagieren, kann es eine gute Idee sein, lokal zu arbeiten, ohne Live-Dienste bereitzustellen.
Ein Realtime Database Emulator ist Teil der Local Emulator Suite, mit der Ihre App mit dem emulierten Datenbankinhalt und der emulierten Konfiguration sowie optional mit den emulierten Projektressourcen (Funktionen, anderen Datenbanken und Sicherheitsregeln) interagieren kann.
Die Verwendung des Realtime Database Emulators umfasst nur wenige Schritte:
- Fügen Sie der Testkonfiguration Ihrer App eine Codezeile hinzu, um eine Verbindung zum Emulator herzustellen.
- Führen Sie im Stammverzeichnis Ihres lokalen Projektverzeichnisses
firebase emulators:startaus. - Rufen Sie aus dem Prototypcode Ihrer App wie gewohnt mit einem Realtime Database Plattform SDK oder mit der Realtime Database REST API auf.
Eine detaillierte Anleitung zur Verwendung von Realtime Database und Cloud Functions ist verfügbar. Lesen Sie auch die Local Emulator Suite Einführung.
DatabaseReference abrufen
Wenn Sie Daten aus der Datenbank lesen oder in sie schreiben möchten, benötigen Sie eine Instanz von DatabaseReference:
Kotlin
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Daten schreiben
Grundlegende Schreibvorgänge
Für grundlegende Schreibvorgänge können Sie mit setValue() Daten in einer angegebenen Referenz speichern und alle vorhandenen Daten an diesem Pfad ersetzen. Sie können diese Methode für Folgendes verwenden:
- Übergeben Sie Typen, die den verfügbaren JSON-Typen entsprechen:
StringLongDoubleBooleanMap<String, Object>List<Object>
- Übergeben Sie ein benutzerdefiniertes Java-Objekt, wenn die Klasse, die es definiert, einen Standardkonstruktor ohne Argumente und öffentliche Getter für die zuzuweisenden Eigenschaften hat.
Wenn Sie ein Java-Objekt verwenden, werden die Inhalte des Objekts automatisch verschachtelt untergeordnete Speicherorte zugeordnet. Die Verwendung eines Java-Objekts macht Ihren Code in der Regel auch besser lesbar und einfacher zu verwalten. Wenn Sie beispielsweise eine App mit einem einfachen Nutzerprofil haben, könnte Ihr User-Objekt so aussehen:
Kotlin
@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; } }
Sie können einen Nutzer mit setValue() so hinzufügen:
Kotlin
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); }
Wenn Sie setValue() auf diese Weise verwenden, werden Daten am angegebenen Speicherort überschrieben, einschließlich aller untergeordneten Knoten. Sie können jedoch weiterhin ein untergeordnetes Element aktualisieren, ohne das gesamte Objekt neu zu schreiben. Wenn Sie Nutzern erlauben möchten, ihre Profile zu aktualisieren, können Sie den Nutzernamen so aktualisieren:
Kotlin
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Daten lesen
Daten mit persistenten Listenern lesen
Wenn Sie Daten an einem Pfad lesen und auf Änderungen reagieren möchten, verwenden Sie die addValueEventListener()
Methode, um einer DatabaseReference einen ValueEventListener hinzuzufügen.
| Listener | Event-Callback | Typische Verwendung |
|---|---|---|
ValueEventListener |
onDataChange() |
Änderungen am gesamten Inhalt eines Pfads lesen und beobachten. |
Mit der Methode onDataChange() können Sie einen statischen Snapshot des Inhalts an einem bestimmten Pfad lesen, so wie er zum Zeitpunkt des Ereignisses vorhanden war. Diese Methode wird einmal ausgelöst, wenn der Listener angehängt wird, und dann jedes Mal, wenn sich die Daten ändern, einschließlich untergeordneter Elemente. An den Event-Callback wird ein Snapshot übergeben, der alle Daten an diesem Speicherort enthält, einschließlich untergeordneter Daten. Wenn keine Daten vorhanden sind, gibt der Snapshot false zurück, wenn Sie exists() aufrufen, und null, wenn Sie getValue() aufrufen.
Im folgenden Beispiel ruft eine Social-Blogging-App die Details eines Beitrags aus der Datenbank ab:
Kotlin
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);
Der Listener empfängt einen DataSnapshot, der die Daten am angegebenen Speicherort in der Datenbank zum Zeitpunkt des Ereignisses enthält. Wenn Sie getValue() für einen Snapshot aufrufen, wird die Java-Objektdarstellung der Daten zurückgegeben. Wenn am Speicherort keine Daten vorhanden sind, gibt getValue() null zurück.
In diesem Beispiel definiert ValueEventListener auch die Methode onCancelled(), die aufgerufen wird, wenn der Lesevorgang abgebrochen wird. Ein Lesevorgang kann beispielsweise abgebrochen werden, wenn der Client keine Berechtigung hat, Daten von einem Firebase-Datenbankspeicherort zu lesen. An diese Methode wird ein DatabaseError-Objekt übergeben, das angibt, warum der Fehler aufgetreten ist.
Daten einmal lesen
Einmal mit get() lesen
Das SDK wurde entwickelt, um Interaktionen mit Datenbankservern zu verwalten, unabhängig davon, ob Ihre App online oder offline ist.
Im Allgemeinen sollten Sie die oben beschriebenen ValueEventListener-Techniken verwenden, um Daten zu lesen und über Aktualisierungen der Daten vom Backend benachrichtigt zu werden. Die Listener-Techniken reduzieren die Nutzung und die Abrechnung und sind optimiert, um Ihren Nutzern die bestmögliche Erfahrung zu bieten, wenn sie online und offline sind.
Wenn Sie die Daten nur einmal benötigen, können Sie mit get() einen Snapshot der Daten aus der Datenbank abrufen. Wenn get() aus irgendeinem Grund den Serverwert nicht zurückgeben kann, prüft der Client den lokalen Speichercache und gibt einen Fehler zurück, wenn der Wert immer noch nicht gefunden wird.
Die unnötige Verwendung von get() kann die Bandbreitennutzung erhöhen und zu Leistungseinbußen führen. Dies lässt sich vermeiden, indem Sie wie oben gezeigt einen Echtzeit-Listener verwenden.
Kotlin
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()));
}
}
});
Einmal mit einem Listener lesen
In einigen Fällen möchten Sie möglicherweise, dass der Wert aus dem lokalen Cache sofort zurückgegeben wird, anstatt auf einen aktualisierten Wert auf dem Server zu warten. In diesen Fällen können Sie addListenerForSingleValueEvent verwenden, um die Daten sofort aus dem lokalen Datenträger-Cache abzurufen.
Dies ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder keine aktive Überwachung erfordern. Die Blogging-App in den vorherigen Beispielen verwendet diese Methode beispielsweise, um das Profil eines Nutzers zu laden, wenn er einen neuen Beitrag verfasst.
Daten aktualisieren oder löschen
Bestimmte Felder aktualisieren
Wenn Sie gleichzeitig in bestimmte untergeordnete Elemente eines Knotens schreiben möchten, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die Methode updateChildren().
Wenn Sie updateChildren() aufrufen, können Sie untergeordnete Werte auf niedrigerer Ebene aktualisieren, indem Sie
einen Pfad für den Schlüssel angeben. Wenn Daten zur besseren Skalierung an mehreren Speicherorten gespeichert sind, können Sie alle Instanzen dieser Daten mit
Data Fan-Outaktualisieren. Eine Social-Blogging-App könnte beispielsweise eine Post-Klasse wie diese haben:
Kotlin
@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; } }
Um einen Beitrag zu erstellen und ihn gleichzeitig im Feed für die letzten Aktivitäten und im Aktivitätsfeed des Nutzers zu aktualisieren, verwendet die Blogging-App Code wie diesen:
Kotlin
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); }
In diesem Beispiel wird mit push() ein Beitrag im Knoten mit Beiträgen für alle Nutzer unter /posts/$postid erstellt und gleichzeitig der Schlüssel mit getKey() abgerufen. Der Schlüssel kann dann verwendet werden, um einen zweiten Eintrag in den Beiträgen des Nutzers unter /user-posts/$userid/$postid zu erstellen.
Mit diesen Pfaden können Sie mit einem einzigen Aufruf von updateChildren() gleichzeitige Aktualisierungen an mehreren Speicherorten im JSON-Baum vornehmen. So wird beispielsweise in diesem Beispiel der neue Beitrag an beiden Speicherorten erstellt. Gleichzeitige Aktualisierungen, die auf diese Weise vorgenommen werden, sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle Aktualisierungen schlagen fehl.
Abschluss-Callback hinzufügen
Wenn Sie wissen möchten, wann Ihre Daten übernommen wurden, können Sie einen Abschluss-Listener hinzufügen. Sowohl setValue() als auch updateChildren() akzeptieren einen optionalen Abschluss-Listener, der aufgerufen wird, wenn der Schreibvorgang erfolgreich in der Datenbank übernommen wurde. Wenn der Aufruf nicht erfolgreich war, wird an den Listener ein Fehlerobjekt übergeben, das angibt, warum der Fehler aufgetreten ist.
Kotlin
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 // ... } });
Daten löschen
Die einfachste Möglichkeit, Daten zu löschen, besteht darin, removeValue() für eine Referenz auf den Speicherort dieser Daten aufzurufen.
Sie können auch löschen, indem Sie null als Wert für einen anderen Schreibvorgang wie setValue() oder updateChildren() angeben. Sie können diese Technik mit updateChildren() verwenden, um mehrere untergeordnete Elemente mit einem einzigen API-Aufruf zu löschen.
Listener trennen
Callbacks werden entfernt, indem Sie die Methode removeEventListener() für Ihre Firebase-Datenbankreferenz aufrufen.
Wenn ein Listener mehrmals zu einem Datenspeicherort hinzugefügt wurde, wird er für jedes Ereignis mehrmals aufgerufen. Sie müssen ihn genauso oft trennen, um ihn vollständig zu entfernen.
Wenn Sie removeEventListener() für einen übergeordneten Listener aufrufen, werden Listener, die für seine untergeordneten Knoten registriert sind, nicht automatisch entfernt. removeEventListener() muss auch für alle untergeordneten Listener aufgerufen werden, um den Callback zu entfernen.
Daten als Transaktionen speichern
Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden könnten, z. B. inkrementelle Zähler, können Sie einen Transaktionsvorgang verwenden. Sie übergeben an diesen Vorgang zwei Argumente: eine Aktualisierungsfunktion und einen optionalen Abschluss-Callback. Die Aktualisierungsfunktion verwendet den aktuellen Status der Daten als Argument und gibt den neuen gewünschten Status zurück, den Sie schreiben möchten. Wenn ein anderer Client an den Speicherort schreibt, bevor Ihr neuer Wert erfolgreich geschrieben wurde, wird Ihre Aktualisierungsfunktion mit dem neuen aktuellen Wert noch einmal aufgerufen und der Schreibvorgang wird wiederholt.
In der Social-Blogging-App im Beispiel könnten Sie Nutzern beispielsweise erlauben, Beiträge mit einem Stern zu markieren und die Markierung wieder zu entfernen, und die Anzahl der Sterne für einen Beitrag so verfolgen:
Kotlin
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); } }); }
Durch die Verwendung einer Transaktion wird verhindert, dass die Anzahl der Sterne falsch ist, wenn mehrere Nutzer gleichzeitig denselben Beitrag mit einem Stern markieren oder der Client veraltete Daten hatte. Wenn die Transaktion abgelehnt wird, gibt der Server den aktuellen Wert an den Client zurück, der die Transaktion mit dem aktualisierten Wert noch einmal ausführt. Dieser Vorgang wird wiederholt, bis die Transaktion akzeptiert wird oder zu viele Versuche unternommen wurden.
Atomare serverseitige Inkremente
In diesem Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Nutzers, der den Beitrag mit einem Stern markiert oder die Markierung entfernt, und die erhöhte Anzahl der Sterne. Wenn wir bereits wissen, dass der Nutzer den Beitrag mit einem Stern markiert, können wir anstelle einer Transaktion einen atomaren Inkrementvorgang verwenden.
Kotlin
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); }
Dieser Code verwendet keinen Transaktionsvorgang. Er wird daher nicht automatisch noch einmal ausgeführt, wenn es zu einer Konfliktsituation kommt. Da der Inkrementvorgang jedoch direkt auf dem Datenbankserver erfolgt, besteht keine Gefahr eines Konflikts.
Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, z. B. wenn ein Nutzer einen Beitrag mit einem Stern markiert, den er bereits zuvor mit einem Stern markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.
Offline mit Daten arbeiten
Wenn ein Client die Netzwerkverbindung verliert, funktioniert Ihre App weiterhin ordnungsgemäß.
Jeder Client, der mit einer Firebase-Datenbank verbunden ist, verwaltet eine eigene interne Version aller Daten, für die Listener verwendet werden oder die so gekennzeichnet sind, dass sie mit dem Server synchronisiert werden. Wenn Daten gelesen oder geschrieben werden, wird zuerst diese lokale Version der Daten verwendet. Der Firebase-Client synchronisiert diese Daten dann nach dem Best-Effort-Prinzip mit den Remote-Datenbankservern und anderen Clients.
Daher lösen alle Schreibvorgänge in die Datenbank sofort lokale Ereignisse aus, bevor eine Interaktion mit dem Server stattfindet. Das bedeutet, dass Ihre App unabhängig von Netzwerklatenz oder Konnektivität reaktionsschnell bleibt.
Sobald die Verbindung wiederhergestellt ist, empfängt Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne dass Sie benutzerdefinierten Code schreiben müssen.
Weitere Informationen zum Offlineverhalten finden Sie unter Online- und Offlinefunktionen.