(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 eine Reihe von Tools vor, mit denen Sie die Funktionen der Realtime Database prototypisieren und testen können: die Firebase Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder nach der kostengünstigsten Möglichkeit suchen, mit dem Back-End zu interagieren, kann es eine gute Idee sein, lokal zu arbeiten, ohne Live-Dienste bereitzustellen.
Ein Realtime Database-Emulator ist Teil der Emulator Suite, mit der Ihre App mit dem emulierten Datenbankinhalt und der emulierten Konfiguration sowie optional mit den emulierten Projektressourcen (Funktionen, andere Datenbanken und Sicherheitsregeln) interagieren kann.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 im Stammverzeichnis Ihres lokalen Projekts
firebase emulators:startaus. - Rufen Sie den Prototypcode Ihrer App mit einem Realtime Database-Plattform-SDK wie gewohnt auf oder verwenden Sie die Realtime Database REST API.
Eine detaillierte Anleitung zur Verwendung von Realtime Database und Cloud Functions ist verfügbar. Sehen Sie sich auch die Einführung in die Emulator Suite an.
DatabaseReference abrufen
Zum Lesen oder Schreiben von Daten aus der Datenbank benötigen Sie eine Instanz von DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
Daten schreiben
In diesem Dokument werden die Grundlagen des Lesens und Schreibens 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 grundlegende Schreibvorgänge können Sie mit set() Daten in einer angegebenen Referenz speichern und alle vorhandenen Daten an diesem Pfad ersetzen. Sie können eine Referenz 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 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:
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 lauschen
Wenn Sie Daten an einem Pfad lesen und auf Änderungen lauschen möchten, verwenden Sie die Eigenschaft onValue von DatabaseReference, um auf DatabaseEvents zu lauschen.
Mit DatabaseEvent können Sie die Daten an einem bestimmten Pfad so 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-Eigenschaft, die alle Daten an diesem Speicherort enthält, einschließlich der untergeordneten Daten. Wenn keine Daten vorhanden sind, ist die Eigenschaft exists des Snapshots false und die Eigenschaft value ist null.
Das folgende Beispiel zeigt eine Social-Blogging-Anwendung, die die Details eines Beitrags aus der Datenbank abruft:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Der Listener empfängt einen DataSnapshot, der die Daten am angegebenen Speicherort in der Datenbank zum Zeitpunkt des Ereignisses in der Eigenschaft value enthält.
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 Techniken für Wertänderungen verwenden, um Daten zu lesen und über Aktualisierungen der Daten vom Back-End benachrichtigt zu werden. Diese Techniken reduzieren die Nutzung und Abrechnung und sind so optimiert, dass Ihre Nutzer die bestmögliche Erfahrung haben, 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.
Das folgende Beispiel zeigt, wie der öffentlich sichtbare 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. Dies lässt sich vermeiden, indem Sie einen Echtzeit-Listener verwenden, wie oben gezeigt.
Daten einmal mit once() 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 mit once() die Daten sofort aus dem lokalen Festplattencache abrufen.
Dies ist nützlich für Daten, die nur einmal geladen werden müssen und sich voraussichtlich nicht häufig ändern oder kein aktives Lauschen erfordern. In den vorherigen Beispielen verwendet die Blogging-App 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 Elemente 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 Speicherorten gespeichert sind, können Sie alle Instanzen dieser Daten mit
Data Fan-Outaktualisieren. Beispielsweise möchte eine Social-Blogging-App möglicherweise einen Beitrag erstellen und ihn gleichzeitig im Feed für die letzten Aktivitäten und im Feed für die Aktivitäten des Nutzers veröffentlichen. 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 mit push() ein Beitrag im Knoten mit Beiträgen für alle Nutzer unter /posts/$postid erstellt und gleichzeitig der Schlüssel mit key 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 update() gleichzeitige Aktualisierungen an mehreren Speicherorten im JSON-Baum vornehmen. In diesem Beispiel wird 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 Abschluss-Callbacks registrieren. Sowohl set() als auch update() geben Futures zurück, an die Sie Erfolgs- und Fehler-Callbacks anhängen können, die aufgerufen werden, wenn der Schreibvorgang in der Datenbank übernommen wurde und wenn der Aufruf nicht erfolgreich war.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Daten löschen
Die einfachste Möglichkeit, Daten zu löschen, besteht darin, remove() für eine Referenz auf den Speicherort dieser Daten aufzurufen.
Sie können auch löschen, indem Sie für einen anderen Schreibvorgang wie set() oder update() null angeben. Mit dieser Technik können Sie mit update() mehrere untergeordnete Elemente in einem einzigen API-Aufruf 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
Transaktions-Handler an runTransaction() übergeben. Ein Transaktions-Handler 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:
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 Transaktions-Aktualisierungsfunktion ausgeführt wird. Wenn Sie die Funktion also mehrmals ausführen, werden möglicherweise Zwischenzustände angezeigt.
Sie können applyLocally auf false setzen, um diese Zwischenzustände 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 Abschluss 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 Inkremente
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 Stern 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);
}
Dieser Code verwendet keinen Transaktionsvorgang. Er wird also nicht automatisch noch einmal ausgeführt, wenn es eine Konfliktaktualisierung gibt. 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 aktiven Daten. Wenn Daten geschrieben werden, werden sie zuerst in diese lokale Version geschrieben. Der Firebase-Client synchronisiert diese Daten dann nach bestem Bemühen mit den Remote-Datenbankservern und mit 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 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.