Dieses Dokument behandelt die Grundlagen des Lesens und Schreibens von Firebase-Daten.
Firebase-Daten werden in eine FirebaseDatabase
Referenz geschrieben und durch Anhängen eines asynchronen Listeners an die Referenz abgerufen. Der Listener wird einmal für den Anfangszustand der Daten und jedes Mal, wenn sich die Daten ändern, erneut ausgelöst.
(Optional) Prototypen erstellen und mit der Firebase Local Emulator Suite testen
Bevor wir darüber sprechen, wie Ihre App aus der Echtzeitdatenbank liest und in sie schreibt, stellen wir eine Reihe von Tools vor, mit denen Sie die Funktionalität der Echtzeitdatenbank prototypisieren und testen können: Firebase Local Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder daran arbeiten, die kostengünstigste Möglichkeit für die Interaktion mit dem Back-End zu finden, kann die Möglichkeit, lokal zu arbeiten, ohne Live-Dienste bereitzustellen, eine gute Idee sein.
Ein Echtzeitdatenbank-Emulator ist Teil der Local Emulator Suite, der es Ihrer App ermöglicht, mit Ihrem emulierten Datenbankinhalt und Ihrer emulierten Datenbankkonfiguration sowie optional mit Ihren emulierten Projektressourcen (Funktionen, andere Datenbanken und Sicherheitsregeln) zu interagieren.
Die Verwendung des Echtzeitdatenbank-Emulators erfordert 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:start
. - Aufrufe aus dem Prototypcode Ihrer App wie gewohnt mit einem Realtime Database-Plattform-SDK oder mit der Realtime Database REST API durchführen.
Eine detaillierte Anleitung zu Echtzeitdatenbank- und Cloud-Funktionen ist verfügbar. Sie sollten sich auch die Einführung zur Local Emulator Suite ansehen.
Holen Sie sich eine Datenbankreferenz
Um Daten aus der Datenbank zu lesen oder zu schreiben, benötigen Sie eine Instanz von DatabaseReference
:
Kotlin+KTX
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Daten schreiben
Grundlegende Schreiboperationen
Für grundlegende Schreibvorgänge können Sie setValue()
verwenden, um Daten in einer angegebenen Referenz zu speichern und alle vorhandenen Daten in diesem Pfad zu ersetzen. Mit dieser Methode können Sie Folgendes tun:
- Übergeben Sie Typen, die den verfügbaren JSON-Typen entsprechen, wie folgt:
-
String
-
Long
-
Double
-
Boolean
-
Map<String, Object>
-
List<Object>
-
- Übergeben Sie ein benutzerdefiniertes Java-Objekt, wenn die Klasse, die es definiert, über einen Standardkonstruktor verfügt, der keine Argumente akzeptiert und über öffentliche Getter für die zuzuweisenden Eigenschaften verfügt.
Wenn Sie ein Java-Objekt verwenden, werden die Inhalte Ihres Objekts automatisch in verschachtelter Form den untergeordneten Speicherorten zugeordnet. Durch die Verwendung eines Java-Objekts wird Ihr Code normalerweise auch besser lesbar und einfacher zu warten. Wenn Sie beispielsweise eine App mit einem einfachen Benutzerprofil haben, könnte Ihr User
wie folgt aussehen:
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; } }
Sie können einen Benutzer mit setValue()
wie folgt hinzufügen:
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); }
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 Objekt aktualisieren, ohne das gesamte Objekt neu zu schreiben. Wenn Sie Benutzern erlauben möchten, ihre Profile zu aktualisieren, können Sie den Benutzernamen wie folgt aktualisieren:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Daten lesen
Lesen Sie Daten mit persistenten Listenern
Um Daten an einem Pfad zu lesen und auf Änderungen zu warten, verwenden Sie die Methode addValueEventListener()
, um einen ValueEventListener
zu einer DatabaseReference
hinzuzufügen.
Hörer | Ereignisrückruf | Typische Verwendung |
---|---|---|
ValueEventListener | onDataChange() | Lesen und achten Sie auf Änderungen am gesamten Inhalt eines Pfads. |
Sie können die Methode onDataChange()
verwenden, um eine statische Momentaufnahme des Inhalts in einem bestimmten Pfad zu lesen, so wie er zum Zeitpunkt des Ereignisses vorhanden war. Diese Methode wird einmal ausgelöst, wenn der Listener angehängt wird, und jedes Mal erneut, wenn sich die Daten, einschließlich untergeordneter Daten, ändern. Dem Ereignisrückruf 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()
dafür aufrufen.
Das folgende Beispiel zeigt eine Social-Blogging-Anwendung, die die Details eines Beitrags aus der Datenbank abruft:
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);
Der Listener empfängt einen DataSnapshot
, der die Daten am angegebenen Ort in der Datenbank zum Zeitpunkt des Ereignisses enthält. Der Aufruf von getValue()
für einen Snapshot gibt die Java-Objektdarstellung der Daten zurück. Wenn am Speicherort keine Daten vorhanden sind, gibt der Aufruf von getValue()
null
zurück.
In diesem Beispiel definiert ValueEventListener
auch die Methode onCancelled()
, die aufgerufen wird, wenn der Lesevorgang abgebrochen wird. Beispielsweise kann ein Lesevorgang abgebrochen werden, wenn der Client keine Berechtigung zum Lesen aus einem Firebase-Datenbankspeicherort hat. Dieser Methode wird ein DatabaseError
Objekt übergeben, das angibt, warum der Fehler aufgetreten ist.
Daten einmal lesen
Einmal lesen mit get()
Das SDK ist für die Verwaltung von Interaktionen mit Datenbankservern konzipiert, 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 Ihren Verbrauch und Ihre Kosten und sind optimiert, um Ihren Benutzern das beste Erlebnis zu bieten, wenn sie online und offline gehen.
Wenn Sie die Daten nur einmal benötigen, können Sie 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 eine Fehlermeldung zurück, wenn der Wert immer noch nicht gefunden wird.
Die unnötige Verwendung von get()
kann die Nutzung der Bandbreite erhöhen und zu Leistungseinbußen führen, was durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt verhindert werden kann.
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()));
}
}
});
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 dem Server nach einem aktualisierten Wert zu suchen. In diesen Fällen können Sie addListenerForSingleValueEvent
verwenden, um die Daten sofort aus dem lokalen Festplatten-Cache abzurufen.
Dies ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder eine aktive Überwachung erfordern. Beispielsweise verwendet die Blogging-App in den vorherigen Beispielen diese Methode, um das Profil eines Benutzers zu laden, wenn dieser mit dem Verfassen eines neuen Beitrags beginnt.
Daten aktualisieren oder löschen
Aktualisieren Sie bestimmte Felder
Um gleichzeitig auf bestimmte untergeordnete Knoten eines Knotens zu schreiben, 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 Standorten gespeichert werden, können Sie alle Instanzen dieser Daten mithilfe von Daten-Fanout aktualisieren. Beispielsweise könnte eine Social-Blogging-App eine Post
Klasse wie diese haben:
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; } }
Um einen Beitrag zu erstellen und ihn gleichzeitig mit dem aktuellen Aktivitäts-Feed und dem Aktivitäts-Feed des postenden Benutzers zu aktualisieren, verwendet die Blogging-Anwendung Code wie diesen:
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); }
In diesem Beispiel wird push()
verwendet, um einen Beitrag im Knoten zu erstellen, der Beiträge für alle Benutzer unter /posts/$postid
enthält, und gleichzeitig den Schlüssel mit getKey()
abzurufen. Der Schlüssel kann dann verwendet werden, um einen zweiten Eintrag in den Beiträgen des Benutzers unter /user-posts/$userid/$postid
zu erstellen.
Mit diesen Pfaden können Sie mit einem einzigen Aufruf von updateChildren()
gleichzeitige Aktualisierungen an mehreren Stellen im JSON-Baum durchführen, so wie in diesem Beispiel der neue Beitrag an beiden Stellen erstellt wird. Auf diese Weise durchgeführte gleichzeitige Aktualisierungen sind atomar: Entweder sind alle Aktualisierungen erfolgreich oder alle Aktualisierungen schlagen fehl.
Fügen Sie einen Abschlussrückruf hinzu
Wenn Sie wissen möchten, wann Ihre Daten festgeschrieben wurden, können Sie einen Abschluss-Listener hinzufügen. Sowohl setValue()
als auch updateChildren()
verwenden einen optionalen Abschluss-Listener, der aufgerufen wird, wenn der Schreibvorgang erfolgreich in die Datenbank übernommen wurde. Wenn der Aufruf nicht erfolgreich war, wird dem Listener ein Fehlerobjekt übergeben, das angibt, warum der Fehler aufgetreten ist.
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 // ... } });
Daten löschen
Der einfachste Weg, Daten zu löschen, besteht darin removeValue()
für einen Verweis auf den Speicherort dieser Daten aufzurufen.
Sie können auch löschen, indem Sie null
als Wert für einen anderen Schreibvorgang angeben, z. B. setValue()
oder updateChildren()
. Sie können diese Technik mit updateChildren()
verwenden, um mehrere untergeordnete Elemente in einem einzigen API-Aufruf zu löschen.
Trennen Sie Zuhörer ab
Rückrufe werden durch Aufrufen der Methode removeEventListener()
in Ihrer Firebase-Datenbankreferenz entfernt.
Wenn ein Listener mehrmals zu einem Datenspeicherort hinzugefügt wurde, wird er für jedes Ereignis mehrmals aufgerufen und Sie müssen ihn genauso oft trennen, um ihn vollständig zu entfernen.
Durch den Aufruf von removeEventListener()
für einen übergeordneten Listener werden die auf seinen untergeordneten Knoten registrierten Listener nicht automatisch entfernt. removeEventListener()
muss auch für alle untergeordneten Listener aufgerufen werden, um den Rückruf 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 eine Transaktionsoperation verwenden. Sie geben dieser Operation zwei Argumente: eine Aktualisierungsfunktion und einen optionalen Abschlussrückruf. Die Update-Funktion nimmt den aktuellen Zustand der Daten als Argument und gibt den neuen gewünschten Zustand 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 erneut mit dem neuen aktuellen Wert aufgerufen und der Schreibvorgang wird wiederholt.
In der Beispiel-Social-Blogging-App könnten Sie Benutzern beispielsweise erlauben, Beiträge zu markieren bzw. die Markierung aufzuheben und zu verfolgen, wie viele Sterne ein Beitrag wie folgt erhalten hat:
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); } }); }
Mithilfe einer Transaktion wird verhindert, dass die Anzahl der Sterne falsch ist, wenn mehrere Benutzer gleichzeitig denselben Beitrag markieren oder der Kunde über veraltete Daten verfügt. Wenn die Transaktion abgelehnt wird, gibt der Server den aktuellen Wert an den Client zurück, der die Transaktion erneut mit dem aktualisierten Wert ausführt. Dies wiederholt sich, bis die Transaktion akzeptiert wird oder zu viele Versuche unternommen wurden.
Atomare serverseitige Inkremente
Im obigen Anwendungsfall schreiben wir zwei Werte in die Datenbank: die ID des Benutzers, der den Beitrag markiert bzw. die Markierung aufhebt, und die erhöhte Anzahl der Sterne. Wenn wir bereits wissen, dass der Benutzer den Beitrag markiert, können wir anstelle einer Transaktion eine atomare Inkrementierungsoperation verwenden.
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); }
Dieser Code verwendet keinen Transaktionsvorgang und wird daher nicht automatisch erneut ausgeführt, wenn es zu einem Konflikt mit der Aktualisierung kommt. Da der Inkrementierungsvorgang jedoch direkt auf dem Datenbankserver erfolgt, besteht keine Möglichkeit eines Konflikts.
Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, beispielsweise wenn ein Benutzer einen Beitrag markiert, den er bereits zuvor markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.
Arbeiten Sie offline mit Daten
Wenn ein Client seine Netzwerkverbindung verliert, funktioniert Ihre App weiterhin ordnungsgemäß.
Jeder Client, der mit einer Firebase-Datenbank verbunden ist, verwaltet seine eigene interne Version aller Daten, für die Listener verwendet werden oder die für die Synchronisierung mit dem Server gekennzeichnet sind. Beim Lesen oder Schreiben von Daten 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 mit anderen Clients.
Daher lösen alle Schreibvorgänge in die Datenbank unmittelbar vor der Interaktion mit dem Server lokale Ereignisse aus. Das bedeutet, dass Ihre App unabhängig von Netzwerklatenz oder Konnektivität reaktionsfähig bleibt.
Sobald die Verbindung wiederhergestellt ist, empfängt Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne dass benutzerdefinierter Code geschrieben werden muss.
Weitere Informationen zum Offline-Verhalten finden Sie unter Weitere Informationen zu Online- und Offline-Funktionen .