Odczyt i zapis danych

(Opcjonalnie) Tworzenie prototypów i testowanie za pomocą Pakietu emulatorów Firebase

Zanim omówimy, jak aplikacja odczytuje dane z Bazy danych czasu rzeczywistego i zapisuje w niej dane, przedstawimy zestaw narzędzi, których możesz używać do prototypowania i testowania funkcji Bazy danych czasu rzeczywistego: Pakiet emulatorów Firebase. Jeśli testujesz różne modele danych, optymalizujesz reguły zabezpieczeń lub szukasz najbardziej opłacalnego sposobu interakcji z backendem, praca lokalna bez wdrażania usług na żywo może być świetnym pomysłem.

Emulator Bazy danych czasu rzeczywistego jest częścią Pakietu emulatorów, który umożliwia aplikacji interakcję z emulowaną zawartością i konfiguracją bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami zabezpieczeń).emulator_suite_short

Korzystanie z emulatora bazy danych czasu rzeczywistego obejmuje tylko kilka kroków:

  1. Dodanie do konfiguracji testowej aplikacji wiersza kodu, który połączy ją z emulatorem.
  2. Uruchom firebase emulators:start w katalogu głównym projektu lokalnego.
  3. Wykonuj wywołania z kodu prototypu aplikacji za pomocą pakietu SDK platformy Baza danych czasu rzeczywistego lub interfejsu Baza danych czasu rzeczywistego REST API.

Dostępny jest szczegółowy przewodnik dotyczący Bazy danych czasu rzeczywistego i Cloud Functions. Zapoznaj się też z wprowadzeniem do Pakietu emulatorów.

Pobieranie obiektu DatabaseReference

Aby odczytywać lub zapisywać dane w bazie danych, potrzebujesz instancji DatabaseReference:

DatabaseReference ref = FirebaseDatabase.instance.ref();

Zapisywanie danych

W tym dokumencie znajdziesz podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.

Dane Firebase są zapisywane w DatabaseReference i pobierane przez oczekiwanie na zdarzenia emitowane przez odwołanie lub ich nasłuchiwanie. Zdarzenia są emitowane raz w przypadku stanu początkowego danych i ponownie za każdym razem, gdy dane się zmienią.

Podstawowe operacje zapisu

W przypadku podstawowych operacji zapisu możesz użyć set(), aby zapisać dane w określonym odwołaniu, zastępując wszystkie istniejące dane w tej ścieżce. Możesz ustawić odwołanie do tych typów: String, boolean, int, double, Map, List.

Możesz na przykład dodać użytkownika z adresem set() w ten sposób:

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

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

Użycie set() w ten sposób spowoduje zastąpienie danych w określonej lokalizacji, w tym wszystkich węzłów podrzędnych. Możesz jednak zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie profili, możesz zaktualizować nazwę użytkownika w ten sposób:

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

// Only update the age, leave the name and address!
await ref.update({
  "age": 19,
});

Metoda update() akceptuje ścieżkę podrzędną do węzłów, co umożliwia aktualizowanie wielu węzłów w bazie danych jednocześnie:

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

await ref.update({
  "123/age": 19,
  "123/address/line1": "1 Mountain View",
});

Odczytywanie danych

Odczytywanie danych przez nasłuchiwanie zdarzeń wartości

Aby odczytać dane w ścieżce i nasłuchiwać zmian, użyj właściwości onValue obiektu DatabaseReference, aby nasłuchiwać zdarzeń DatabaseEvent.

Za pomocą DatabaseEvent możesz odczytać dane w określonej ścieżce, które istniały w momencie wystąpienia zdarzenia. To zdarzenie jest wywoływane raz po dołączeniu odbiornika i ponownie za każdym razem, gdy zmienią się dane, w tym wszystkie elementy podrzędne. Zdarzenie ma właściwość snapshot zawierającą wszystkie dane z tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, właściwość exists w migawce będzie miała wartość false, a właściwość value będzie miała wartość null.

Poniższy przykład pokazuje, jak aplikacja do blogowania społecznościowego pobiera z bazy danych szczegóły posta:

DatabaseReference starCountRef =
        FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
    final data = event.snapshot.value;
    updateStarCount(data);
});

Detektor otrzymuje obiekt DataSnapshot, który zawiera dane z określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia w jego właściwości value.

Odczytywanie danych tylko raz

Odczytywanie danych za pomocą metody get()

Pakiet SDK jest przeznaczony do zarządzania interakcjami z serwerami baz danych niezależnie od tego, czy aplikacja jest online czy offline.

Ogólnie rzecz biorąc, do odczytywania danych i otrzymywania powiadomień o aktualizacjach danych z backendu należy używać opisanych powyżej technik zdarzeń wartości. Te techniki zmniejszają zużycie danych i rachunki oraz są zoptymalizowane pod kątem zapewnienia użytkownikom jak największej wygody podczas korzystania z internetu i pracy w trybie offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć get(), aby uzyskać zrzut danych z bazy danych. Jeśli z jakiegokolwiek powodu get() nie może zwrócić wartości serwera, klient sprawdzi pamięć podręczną lokalnego magazynu i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Ten przykład pokazuje, jak jednorazowo pobrać z bazy danych publiczną nazwę użytkownika:

final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
    print(snapshot.value);
} else {
    print('No data available.');
}

Niepotrzebne użycie get() może zwiększyć wykorzystanie przepustowości i spowodować utratę wydajności. Można temu zapobiec, używając odbiornika w czasie rzeczywistym, jak pokazano powyżej.

Jednorazowe odczytywanie danych za pomocą funkcji once()

W niektórych przypadkach możesz chcieć, aby wartość z pamięci podręcznej była zwracana natychmiast, zamiast sprawdzać zaktualizowaną wartość na serwerze. W takich przypadkach możesz użyć once(), aby natychmiast pobrać dane z lokalnej pamięci podręcznej dysku.

Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i które nie powinny się często zmieniać ani wymagać aktywnego nasłuchiwania. Na przykład aplikacja do blogowania z poprzednich przykładów używa tej metody do wczytywania profilu użytkownika, gdy zaczyna on pisać nowy post:

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

Aktualizowanie i usuwanie danych

Aktualizowanie określonych pól

Aby jednocześnie zapisywać dane w określonych węzłach podrzędnych bez zastępowania innych węzłów podrzędnych, użyj metody update().

Podczas wywoływania funkcji update() możesz aktualizować wartości podrzędne niższego poziomu, podając ścieżkę do klucza. Jeśli dane są przechowywane w wielu lokalizacjach, aby lepiej skalować system, możesz zaktualizować wszystkie instancje tych danych za pomocą zwielokrotnienia wyjściowego danych. Na przykład aplikacja do blogowania społecznościowego może chcieć utworzyć post i jednocześnie zaktualizować go w kanale ostatniej aktywności i w kanale aktywności użytkownika, który go opublikował. Aby to zrobić, aplikacja do blogowania używa takiego kodu:

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);
}

W tym przykładzie używamy push(), aby utworzyć post w węźle zawierającym posty wszystkich użytkowników pod adresem /posts/$postid, i jednocześnie pobrać klucz za pomocą key. Klucz może być następnie użyty do utworzenia drugiego wpisu na koncie użytkownika w /user-posts/$userid/$postid.

Korzystając z tych ścieżek, możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania update(), np. w tym przykładzie, w którym nowy post jest tworzony w obu lokalizacjach. Jednoczesne aktualizacje przeprowadzane w ten sposób są niepodzielne: albo wszystkie się udają, albo wszystkie się nie udają.

Dodawanie wywołania zwrotnego po zakończeniu

Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz zarejestrować wywołania zwrotne po zakończeniu. Zarówno set(), jak i update() zwracają Futures, do których możesz dołączyć wywołania zwrotne dotyczące powodzenia i błędu, które są wywoływane, gdy zapis zostanie zatwierdzony w bazie danych i gdy wywołanie zakończy się niepowodzeniem.

FirebaseDatabase.instance
    .ref('users/$userId/email')
    .set(emailAddress)
    .then((_) {
        // Data saved successfully!
    })
    .catchError((error) {
        // The write failed...
    });

Usuń dane

Najprostszym sposobem usunięcia danych jest wywołanie funkcji remove() na odwołaniu do lokalizacji tych danych.

Możesz też usunąć element, podając wartość null w przypadku innej operacji zapisu, np. set() lub update(). Możesz użyć tej techniki z update(), aby usunąć wiele kont dzieci w ramach jednego wywołania interfejsu API.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą zostać uszkodzone przez równoczesne modyfikacje, np. liczniki przyrostowe, możesz użyć transakcji, przekazując do funkcji runTransaction() moduł obsługi transakcji. Procedura obsługi transakcji przyjmuje jako argument bieżący stan danych i zwraca nowy stan, który chcesz zapisać. Jeśli inny klient zapisze dane w tej lokalizacji, zanim nowa wartość zostanie zapisana, funkcja aktualizacji zostanie ponownie wywołana z nową bieżącą wartością, a zapis zostanie ponowiony.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i cofanie tego oznaczenia oraz śledzić liczbę gwiazdek, które otrzymał post, w ten sposób:

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);
  });
}

Domyślnie zdarzenia są wywoływane za każdym razem, gdy uruchamiana jest funkcja aktualizacji transakcji. Jeśli uruchomisz ją kilka razy, możesz zobaczyć stany pośrednie. Możesz ustawić wartość applyLocally na false, aby pominąć te stany pośrednie i zamiast tego poczekać, aż transakcja się zakończy, zanim zostaną wywołane zdarzenia:

await ref.runTransaction((Object? post) {
  // ...
}, applyLocally: false);

Wynikiem transakcji jest TransactionResult, który zawiera informacje, takie jak to, czy transakcja została zatwierdzona, oraz nowy 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

Anulowanie transakcji

Jeśli chcesz bezpiecznie anulować transakcję, zadzwoń pod numer Transaction.abort(), aby zgłosić wyjątek AbortTransactionException:

TransactionResult result = await ref.runTransaction((Object? user) {
  if (user !== null) {
    return Transaction.abort();
  }

  // ...
});

print(result.committed); // false

Atomowe zwiększanie wartości po stronie serwera

W powyższym przypadku użycia zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który dodaje lub usuwa gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznaczył posta gwiazdką, możemy użyć operacji przyrostu atomowego zamiast transakcji.

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);
}

Ten kod nie korzysta z operacji transakcji, więc nie jest automatycznie uruchamiany ponownie w przypadku sprzecznej aktualizacji. Jednak ponieważ operacja zwiększania odbywa się bezpośrednio na serwerze bazy danych, nie ma możliwości wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty związane z aplikacją, np. gdy użytkownik oznaczy gwiazdką post, który już wcześniej oznaczył, napisz niestandardowe reguły bezpieczeństwa dla tego przypadku użycia.

Praca z danymi offline

Jeśli klient utraci połączenie z siecią, aplikacja będzie nadal działać prawidłowo.

Każdy klient połączony z bazą danych Firebase ma własną wewnętrzną wersję wszystkich aktywnych danych. Gdy dane są zapisywane, najpierw trafiają do tej lokalnej wersji. Klient Firebase synchronizuje te dane z serwerami zdalnej bazy danych i innymi klientami w miarę możliwości.

W rezultacie wszystkie zapisy w bazie danych natychmiast wywołują lokalne zdarzenia, zanim jakiekolwiek dane zostaną zapisane na serwerze. Oznacza to, że aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub połączenia.

Po ponownym nawiązaniu połączenia aplikacja otrzymuje odpowiedni zestaw zdarzeń, dzięki czemu klient synchronizuje się z bieżącym stanem serwera bez konieczności pisania niestandardowego kodu.

Więcej informacji o zachowaniach offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.

Dalsze kroki