(Optional) Prototypen erstellen und testen mit der Firebase Emulator Suite
Bevor wir darauf eingehen, wie Ihre App Daten aus der Realtime Database liest und in sie schreibt, stellen wir Ihnen die Firebase Emulator Suite vor. Mit dieser Reihe von Tools können Sie Prototypen erstellen und die Realtime Database-Funktionen testen. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder nach der kostengünstigsten Möglichkeit suchen, mit dem Backend zu interagieren, kann es sinnvoll sein, lokal zu arbeiten, ohne Live-Dienste bereitzustellen.
Ein Realtime Database-Emulator ist Teil der Emulator Suite. Damit kann Ihre App mit den emulierten Datenbankinhalten und der Konfiguration sowie optional mit den emulierten Projektressourcen (Funktionen, anderen Datenbanken und Sicherheitsregeln) interagieren.emulator_suite_short
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
firebase emulators:startim Stammverzeichnis Ihres lokalen Projektverzeichnisses aus. - Aufrufe aus dem Prototypcode Ihrer App mit einem Realtime Database-Plattform-SDK wie gewohnt oder mit der Realtime Database REST API.
Eine detaillierte Anleitung mit Realtime Database und Cloud Functions ist verfügbar. Sehen Sie sich auch die Einführung in die Emulator Suite an.
DatabaseReference abrufen
Wenn Sie Daten aus der Datenbank lesen oder in die Datenbank schreiben möchten, benötigen Sie eine Instanz von DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
Daten schreiben
In diesem Dokument werden die Grundlagen zum Lesen und Schreiben von Firebase-Daten behandelt.
Firebase-Daten werden in eine DatabaseReference geschrieben und abgerufen, indem auf Ereignisse gewartet oder auf Ereignisse gelauscht wird, die von der Referenz ausgegeben werden. Ereignisse werden einmal für den anfänglichen Status der Daten und dann bei jeder Änderung der Daten ausgegeben.
Grundlegende Schreibvorgänge
Für einfache Schreibvorgänge können Sie set() verwenden, um Daten an einem angegebenen Verweis zu speichern und alle vorhandenen Daten an diesem Pfad zu ersetzen. Sie können einen Verweis auf die folgenden Typen festlegen: String, boolean, int, double, Map, List.
So können Sie beispielsweise einen Nutzer mit set() hinzufügen:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
Wenn Sie set() auf diese Weise verwenden, werden Daten am angegebenen Speicherort überschrieben, einschließlich aller untergeordneten Knoten. Sie können ein untergeordnetes Element jedoch weiterhin 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:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
Die Methode update() akzeptiert einen Unterpfad zu Knoten, sodass Sie mehrere Knoten in der Datenbank gleichzeitig aktualisieren können:
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
Daten lesen
Daten lesen, indem Sie auf Wertänderungen warten
Wenn Sie Daten an einem Pfad lesen und auf Änderungen reagieren möchten, verwenden Sie die onValue-Eigenschaft von DatabaseReference, um auf DatabaseEvents zu reagieren.
Mit DatabaseEvent können Sie die Daten an einem bestimmten Pfad lesen, wie sie zum Zeitpunkt des Ereignisses vorhanden sind. Dieses Ereignis wird einmal ausgelöst, wenn der Listener angehängt wird, und dann jedes Mal, wenn sich die Daten ändern, einschließlich aller untergeordneten Elemente. Das Ereignis hat eine snapshot-Property, die alle Daten an diesem Standort enthält, einschließlich untergeordneter Daten. Wenn keine Daten vorhanden sind, ist das Attribut exists des Snapshots false und das Attribut value ist null.
Im folgenden Beispiel ruft eine Social-Blogging-Anwendung die Details eines Beitrags aus der Datenbank ab:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Der Listener empfängt ein DataSnapshot, das die Daten am angegebenen Speicherort in der Datenbank zum Zeitpunkt des Ereignisses in seiner value-Eigenschaft enthält.
Daten einmal lesen
Einmaliges Lesen mit get()
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 Techniken für Wert-Ereignisse verwenden, um Daten zu lesen und über Aktualisierungen der Daten aus dem Backend benachrichtigt zu werden. Diese Techniken reduzieren die Nutzung und Abrechnung und sind darauf ausgelegt, Ihren Nutzern das bestmögliche Erlebnis 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, fragt der Client den lokalen Speicher-Cache ab und gibt einen Fehler zurück, wenn der Wert immer noch nicht gefunden wird.
Das folgende Beispiel zeigt, wie der öffentliche Nutzername eines Nutzers einmal aus der Datenbank abgerufen wird:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
Die unnötige Verwendung von get() kann die Bandbreitennutzung erhöhen und zu Leistungseinbußen führen. Das lässt sich durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt vermeiden.
Daten einmal mit once() lesen
In einigen Fällen soll der Wert aus dem lokalen Cache sofort zurückgegeben werden, anstatt auf dem Server nach einem aktualisierten Wert zu suchen. In diesen Fällen können Sie once() verwenden, um die Daten sofort aus dem lokalen Datenträger-Cache abzurufen.
Das ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder aktives Zuhören 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:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
Daten aktualisieren oder löschen
Bestimmte Felder aktualisieren
Wenn Sie gleichzeitig in bestimmte untergeordnete Knoten eines Knotens schreiben möchten, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die Methode update().
Wenn Sie update() 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 mit Data Fan-Out aktualisieren. Eine Social-Blogging-App möchte beispielsweise einen Beitrag erstellen und ihn gleichzeitig im Feed für die letzten Aktivitäten und im Aktivitätsfeed des Nutzers, der den Beitrag erstellt hat, aktualisieren. Dazu verwendet die Blogging-Anwendung Code wie diesen:
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
In diesem Beispiel wird push() verwendet, um einen Beitrag im Knoten mit Beiträgen für alle Nutzer unter /posts/$postid zu erstellen und gleichzeitig den Schlüssel mit key abzurufen. 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 update() mehrere Standorte im JSON-Baum gleichzeitig aktualisieren. Im folgenden Beispiel wird der neue Beitrag an beiden Standorten erstellt. Gleichzeitige Aktualisierungen auf diese Weise 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 Abschluss-Callbacks registrieren. Sowohl set() als auch update() geben Future zurück, an die Sie Erfolgs- und Fehler-Callbacks anhängen können, die aufgerufen werden, wenn der Schreibvorgang in der Datenbank committet wurde und wenn der Aufruf fehlgeschlagen ist.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Daten löschen
Am einfachsten löschen Sie Daten, indem Sie remove() für eine Referenz auf den Speicherort dieser Daten aufrufen.
Sie können auch löschen, indem Sie „null“ als Wert für einen anderen Schreibvorgang wie set() oder update() angeben. Sie können diese Technik mit update() verwenden, um mehrere untergeordnete Elemente in einem einzigen API-Aufruf zu löschen.
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 Transaktion verwenden, indem Sie einen Transaktionshandler an runTransaction() übergeben. Ein Transaktions-Handler verwendet 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 noch einmal mit dem neuen aktuellen Wert aufgerufen und der Schreibvorgang wird wiederholt.
In der Beispiel-App für Social Blogging könnten Sie Nutzern beispielsweise erlauben, Beiträge mit einem Stern zu markieren und die Markierung wieder zu entfernen. Außerdem könnten Sie die Anzahl der Sterne für einen Beitrag so im Blick behalten:
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
Standardmäßig werden Ereignisse jedes Mal ausgelöst, wenn die Funktion zur Aktualisierung von Transaktionen ausgeführt wird. Wenn Sie die Funktion also mehrmals ausführen, sehen Sie möglicherweise Zwischenstatus.
Sie können applyLocally auf false festlegen, um diese Zwischenstatus zu unterdrücken und stattdessen zu warten, bis die Transaktion abgeschlossen ist, bevor Ereignisse ausgelöst werden:
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
Das Ergebnis einer Transaktion ist ein TransactionResult, das Informationen wie den Commit-Status der Transaktion und den neuen Snapshot enthält:
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
Transaktion wird storniert
Wenn Sie eine Transaktion sicher abbrechen möchten, rufen Sie Transaction.abort() auf, um eine AbortTransactionException auszulösen:
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
Atomare serverseitige Steigerungen
Im obigen 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 Sternchen markiert, können wir anstelle einer Transaktion einen atomaren Inkrementvorgang verwenden.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
In diesem Code wird kein Transaktionsvorgang verwendet. Er wird also nicht automatisch noch einmal ausgeführt, wenn es zu einem Konflikt mit einem Update kommt. Da der Inkrementierungsvorgang jedoch direkt auf dem Datenbankserver erfolgt, besteht keine Konfliktgefahr.
Wenn Sie anwendungsspezifische Konflikte erkennen und ablehnen möchten, z. B. wenn ein Nutzer einen Beitrag mit Sternchen markiert, den er bereits zuvor mit Sternchen 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 seine eigene interne Version aller aktiven Daten. Wenn Daten geschrieben werden, werden sie zuerst in diese lokale Version geschrieben. Der Firebase-Client synchronisiert diese Daten dann auf Best-Effort-Basis mit den Remote-Datenbankservern und anderen Clients.
Daher lösen alle Schreibvorgänge in die Datenbank sofort lokale Ereignisse aus, bevor Daten auf den Server geschrieben werden. Das bedeutet, dass Ihre App unabhängig von Netzwerklatenz oder ‑verbindung reaktionsfähig bleibt.
Sobald die Verbindung wiederhergestellt ist, erhält Ihre App die entsprechenden Ereignisse, damit der Client mit dem aktuellen Serverstatus synchronisiert wird. Sie müssen dazu keinen benutzerdefinierten Code schreiben.
Weitere Informationen zum Offlineverhalten finden Sie unter Online- und Offlinefunktionen.