Daten lesen und schreiben

(Optional) Prototyp und Test mit der Firebase Emulator Suite

Bevor wir darüber sprechen, wie Ihre App aus der Realtime Database liest und in sie schreibt, stellen wir Ihnen eine Reihe von Tools vor, mit denen Sie die Realtime Database-Funktionalität prototypisieren und testen können: Firebase Emulator Suite. Wenn Sie verschiedene Datenmodelle ausprobieren, Ihre Sicherheitsregeln optimieren oder daran arbeiten, den kostengünstigsten Weg zur Interaktion mit dem Back-End zu finden, kann es eine gute Idee sein, lokal arbeiten zu können, ohne Live-Dienste bereitzustellen.

Ein Realtime Database-Emulator ist Teil der Emulator Suite, die es Ihrer App ermöglicht, mit Ihren emulierten Datenbankinhalten und -konfigurationen sowie optional mit Ihren emulierten Projektressourcen (Funktionen, andere Datenbanken und Sicherheitsregeln) zu interagieren. emulator_suite_short

Die Verwendung des Realtime Database-Emulators umfasst nur wenige Schritte:

  1. Hinzufügen einer Codezeile zur Testkonfiguration Ihrer App, um eine Verbindung mit dem Emulator herzustellen.
  2. Führen Sie im Stammverzeichnis Ihres lokalen Projektverzeichnisses firebase emulators:start aus.
  3. Aufrufe aus dem Prototypcode Ihrer App wie gewohnt mit einem SDK der Realtime Database-Plattform oder mit der REST-API der Realtime Database.

Eine detaillierte Anleitung zu Realtime Database und Cloud Functions ist verfügbar. Sie sollten sich auch die Einführung in die 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 :

DatabaseReference ref = FirebaseDatabase.instance.ref();

Daten schreiben

Dieses Dokument behandelt die Grundlagen zum Lesen und Schreiben von Firebase-Daten.

Firebase-Daten werden in eine DatabaseReference geschrieben und abgerufen, indem auf Ereignisse gewartet oder überwacht wird, die von der Referenz ausgegeben werden. Ereignisse werden einmal für den Anfangszustand der Daten ausgegeben und erneut, wenn sich die Daten ändern.

Grundlegende Schreiboperationen

Für grundlegende Schreiboperationen können Sie set() verwenden, um Daten in einer angegebenen Referenz zu speichern und alle vorhandenen Daten in diesem Pfad zu ersetzen. Sie können einen Verweis auf die folgenden Typen setzen: String , boolean , int , double , Map , List .

Beispielsweise können Sie einen Benutzer mit set() wie folgt hinzufügen:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

await ref.set({
  "name": "John",
  "age": 18,
  "address": {
    "line1": "100 Mountain View"
  }
});

Die Verwendung von set() auf diese Weise überschreibt Daten an der angegebenen Position, einschließlich aller untergeordneten Knoten. Sie können jedoch 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:

DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");

// Only update the name, leave the age 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

Lesen Sie Daten, indem Sie auf Wertereignisse lauschen

Um Daten in einem Pfad zu lesen und auf Änderungen zu warten, verwenden Sie die Eigenschaft onValue von DatabaseReference , um auf DatabaseEvent s zu lauschen.

Sie können das DatabaseEvent verwenden, um die Daten in einem bestimmten Pfad so zu lesen, wie sie zum Zeitpunkt des Ereignisses vorhanden sind. Dieses Ereignis wird einmal ausgelöst, wenn der Listener angehängt wird, und jedes Mal, wenn sich die Daten, einschließlich aller untergeordneten Elemente, ändern. Das Ereignis hat eine snapshot -Eigenschaft, die alle Daten an diesem Ort enthält, einschließlich untergeordneter Daten. Wenn keine Daten exists sind, ist die Existing-Eigenschaft des Snapshots false und die value -Eigenschaft 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 erhält einen DataSnapshot , der die Daten an der angegebenen Stelle in der Datenbank zum Zeitpunkt des Ereignisses in seiner value -Eigenschaft enthält.

Daten einmal lesen

Einmal 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 Wertereignistechniken verwenden, um Daten zu lesen, um vom Back-End über Aktualisierungen der Daten benachrichtigt zu werden. Diese Techniken reduzieren Ihre Nutzung und Abrechnung und sind optimiert, um Ihren Benutzern die beste Erfahrung zu bieten, wenn sie online und offline gehen.

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 das einmalige Abrufen des öffentlich zugänglichen Benutzernamens eines Benutzers aus der Datenbank:

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 Nutzung der Bandbreite erhöhen und zu Leistungsverlusten führen, was durch die Verwendung eines Echtzeit-Listeners wie oben gezeigt verhindert werden kann.

Daten einmal lesen mit once()

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 once() verwenden, um die Daten sofort aus dem lokalen Festplattencache zu holen.

Dies 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. Beispielsweise verwendet die Blogging-App in den vorherigen Beispielen diese Methode, um das Profil eines Benutzers zu laden, wenn er mit dem Verfassen eines neuen Beitrags beginnt:

final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';

Aktualisieren oder Löschen von Daten

Aktualisieren Sie bestimmte Felder

Um gleichzeitig in bestimmte untergeordnete Knoten eines Knotens zu schreiben, ohne andere untergeordnete Knoten zu überschreiben, verwenden Sie die Methode update() .

Beim Aufrufen von update() 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 werden, können Sie alle Instanzen dieser Daten mithilfe von Datenauffächerung aktualisieren. Beispielsweise möchte eine Social-Blogging-App möglicherweise einen Beitrag erstellen und ihn gleichzeitig mit dem Feed für die letzten Aktivitäten und dem Aktivitätsfeed des postenden Benutzers 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 zu erstellen, der Beiträge für alle Benutzer unter /posts/$postid , und gleichzeitig den Schlüssel mit key 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.

Mithilfe dieser Pfade können Sie mit einem einzigen Aufruf von update() simultane Aktualisierungen an mehreren Stellen in der JSON-Struktur durchführen, z. B. 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 Abschluss-Callback hinzu

Wenn Sie wissen möchten, wann Ihre Daten festgeschrieben wurden, können Sie Abschlussrückrufe registrieren. Sowohl set() als auch update() geben Future s zurück, an die Sie Erfolgs- und Fehlerrückrufe anhängen können, die aufgerufen werden, wenn der Schreibvorgang an die Datenbank übertragen wurde und 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

Der einfachste Weg, Daten zu löschen, besteht darin, remove() 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 wie set() oder update() . Sie können diese Technik mit update() verwenden, um mehrere Kinder in einem einzigen API-Aufruf zu löschen.

Speichern Sie Daten als Transaktionen

Wenn Sie mit Daten arbeiten, die durch gleichzeitige Änderungen beschädigt werden könnten, wie z. B. inkrementelle Zähler, können Sie eine Transaktion verwenden, indem Sie einen Transaktionshandler an runTransaction() . Ein Transaktionshandler nimmt 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 erneut mit dem neuen aktuellen Wert aufgerufen, und der Schreibvorgang wird wiederholt.

In der Beispiel-Social-Blogging-App könnten Sie beispielsweise Benutzern erlauben, Beiträge zu markieren und die Markierung aufzuheben, und nachverfolgen, wie viele Sterne ein Beitrag erhalten hat, wie folgt:

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 Transaktionsaktualisierungsfunktion ausgeführt wird. Wenn Sie die Funktion also mehrmals ausführen, sehen Sie möglicherweise Zwischenzustände. Sie können applyLocally auf false setzen, um diese Zwischenzustände zu unterdrücken und stattdessen 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 darüber enthält, ob die Transaktion festgeschrieben wurde, und den neuen Snapshot:

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

Stornieren einer Transaktion

Wenn Sie eine Transaktion sicher abbrechen möchten, rufen Sie Transaction.abort() auf, um eine AbortTransactionException :

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 Benutzers, der den Beitrag markiert/entmarkiert, 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 Inkrementoperation 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 keine Transaktionsoperation, sodass er nicht automatisch erneut ausgeführt wird, wenn eine widersprüchliche Aktualisierung vorliegt. Da die Inkrementierungsoperation jedoch direkt auf dem Datenbankserver stattfindet, besteht keine Möglichkeit eines Konflikts.

Wenn Sie anwendungsspezifische Konflikte erkennen und zurückweisen möchten, z. B. wenn ein Benutzer einen Beitrag markiert, den er bereits zuvor markiert hat, sollten Sie benutzerdefinierte Sicherheitsregeln für diesen Anwendungsfall schreiben.

Offline mit Daten arbeiten

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 aktiven Daten. Wenn Daten geschrieben werden, werden sie zuerst in diese lokale Version geschrieben. Der Firebase-Client synchronisiert diese Daten dann nach bestem Wissen und Gewissen 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 Konnektivität reaktionsfähig bleibt.

Sobald die Konnektivität wiederhergestellt ist, empfängt Ihre App die entsprechenden Ereignisse, sodass der Client mit dem aktuellen Serverstatus synchronisiert wird, ohne benutzerdefinierten Code schreiben zu müssen.

Wir werden mehr über das Offline-Verhalten in Erfahren Sie mehr über Online- und Offline-Funktionen sprechen.

Nächste Schritte