Odczytywanie i zapisywanie danych

(Opcjonalnie) Wykonaj prototyp i przetestuj za pomocą pakietu Firebase Emulator Suite

Zanim porozmawiamy o tym, jak aplikacja odczytuje i zapisuje w bazie danych Realtime Database, przedstawmy zestaw narzędzi, których można użyć do prototypowania i testowania funkcjonalności bazy danych Realtime Database: Firebase Emulator Suite. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły bezpieczeństwa lub szukasz najbardziej opłacalnego sposobu interakcji z zapleczem, możliwość pracy lokalnej bez wdrażania usług na żywo może być świetnym pomysłem.

Emulator bazy danych czasu rzeczywistego jest częścią pakietu Emulator Suite, który umożliwia interakcję aplikacji z zawartością i konfiguracją emulowanej bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami bezpieczeństwa).emulator_suite_short

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

  1. Dodanie wiersza kodu do konfiguracji testowej aplikacji w celu nawiązania połączenia z emulatorem.
  2. Z katalogu głównego lokalnego katalogu projektu uruchom firebase emulators:start .
  3. Wykonywanie wywołań z kodu prototypu aplikacji przy użyciu standardowego pakietu SDK platformy Realtime Database lub interfejsu API REST bazy danych Realtime.

Dostępny jest szczegółowy przewodnik dotyczący bazy danych czasu rzeczywistego i funkcji chmury . Powinieneś także zapoznać się ze wstępem do pakietu emulatorów .

Uzyskaj odniesienie do bazy danych

Aby odczytać lub zapisać dane z bazy danych, potrzebujesz instancji DatabaseReference :

DatabaseReference ref = FirebaseDatabase.instance.ref();

Zapisz dane

W tym dokumencie opisano podstawy odczytywania i zapisywania danych Firebase.

Dane Firebase są zapisywane w DatabaseReference i pobierane poprzez oczekiwanie lub nasłuchiwanie zdarzeń emitowanych przez odwołanie. Zdarzenia są emitowane raz dla początkowego stanu danych i ponownie przy każdej zmianie danych.

Podstawowe operacje zapisu

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

Na przykład możesz dodać użytkownika za pomocą set() w następujący sposób:

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

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

Użycie metody set() w ten sposób nadpisuje dane w określonej lokalizacji, włączając w to wszystkie węzły podrzędne. Jednak nadal możesz zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz umożliwić użytkownikom aktualizację swoich profili, możesz zaktualizować nazwę użytkownika w następujący sposób:

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

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

Metoda update() akceptuje podścieżkę do węzłów, umożliwiając jednoczesną aktualizację wielu węzłów w bazie danych:

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

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

Przeczytaj dane

Odczytuj dane, nasłuchując zdarzeń wartościowych

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

Możesz użyć zdarzenia DatabaseEvent , aby odczytać dane z danej ścieżki, takiej jaka istnieje w momencie zdarzenia. To zdarzenie jest wyzwalane raz, gdy słuchacz jest podłączony, i ponownie za każdym razem, gdy dane, w tym wszystkie elementy podrzędne, ulegną zmianie. Zdarzenie ma właściwość snapshot zawierającą wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, właściwość migawki exists będzie miała wartość false , a jej właściwość value będzie mieć wartość null.

Poniższy przykład ilustruje aplikację do blogowania społecznościowego pobierającą szczegóły wpisu z bazy danych:

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

Odbiornik odbiera DataSnapshot , który zawiera dane w określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia w swojej właściwości value .

Przeczytaj dane raz

Przeczytaj raz, używając get()

Zestaw SDK służy do zarządzania interakcjami z serwerami baz danych, niezależnie od tego, czy aplikacja jest w trybie online, czy offline.

Ogólnie rzecz biorąc, należy używać opisanych powyżej technik zdarzeń wartości do odczytywania danych i otrzymywania powiadomień o aktualizacjach danych z zaplecza. Techniki te ograniczają wykorzystanie i rozliczenia oraz są zoptymalizowane, aby zapewnić użytkownikom najlepsze doświadczenia podczas korzystania z Internetu i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć get() w celu uzyskania migawki danych z bazy danych. Jeśli z jakiegoś powodu funkcja get() nie może zwrócić wartości serwera, klient sprawdzi lokalną pamięć podręczną i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Poniższy przykład ilustruje jednorazowe pobranie z bazy danych publicznej nazwy 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 funkcji get() może zwiększyć wykorzystanie przepustowości i prowadzić do utraty wydajności, czemu można zapobiec, korzystając z odbiornika działającego w czasie rzeczywistym, jak pokazano powyżej.

Przeczytaj dane raz za pomocą Once()

W niektórych przypadkach możesz chcieć natychmiastowego zwrócenia wartości z lokalnej pamięci podręcznej, zamiast sprawdzać, czy na serwerze jest zaktualizowana wartość. W takich przypadkach możesz użyć once() , aby natychmiast pobrać dane z pamięci podręcznej dysku lokalnego.

Jest to przydatne w przypadku danych, które należy załadować tylko raz i nie oczekuje się, że będą 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 ładowania profilu użytkownika, gdy rozpoczyna on tworzenie nowego posta:

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

Aktualizacja lub usuwanie danych

Zaktualizuj określone pola

Aby jednocześnie pisać do określonych węzłów podrzędnych węzła bez nadpisywania innych węzłów podrzędnych, użyj metody update() .

Wywołując funkcję update() , możesz zaktualizować wartości podrzędne niższego poziomu, określając ścieżkę do klucza. Jeśli dane są przechowywane w wielu lokalizacjach w celu lepszego skalowania, możesz zaktualizować wszystkie wystąpienia tych danych, korzystając z rozdzielania danych . Na przykład aplikacja do blogowania społecznościowego może chcieć utworzyć post i jednocześnie zaktualizować go do kanału ostatniej aktywności i kanału aktywności użytkownika zamieszczającego post. W tym celu aplikacja do blogowania używa następującego 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 zastosowano metodę push() do utworzenia posta w węźle zawierającego posty dla wszystkich użytkowników w /posts/$postid i jednoczesnego pobrania klucza za pomocą key . Klucza można następnie użyć do utworzenia drugiego wpisu w postach użytkownika pod adresem /user-posts/$userid/$postid .

Korzystając z tych ścieżek, możesz wykonywać jednoczesne aktualizacje wielu lokalizacji w drzewie JSON za pomocą jednego wywołania update() , tak jak w tym przykładzie tworzony jest nowy post w obu lokalizacjach. Jednoczesne aktualizacje wykonane w ten sposób mają charakter niepodzielny: albo wszystkie aktualizacje się powiodą, albo wszystkie aktualizacje nie powiodą się.

Dodaj wywołanie zwrotne zakończenia

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, możesz zarejestrować wywołania zwrotne zakończenia. Zarówno set() jak i update() zwracają Future s, do których można dołączyć wywołania zwrotne zakończone sukcesem i błędami, które są wywoływane, gdy zapis został zatwierdzony do bazy danych i gdy wywołanie nie powiodło się.

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

Usunąć dane

Najprostszym sposobem usunięcia danych jest wywołanie metody remove() w odniesieniu do lokalizacji tych danych.

Możesz także usunąć, podając null jako wartość innej operacji zapisu, takiej jak set() lub update() . Możesz użyć tej techniki z update() , aby usunąć wiele dzieci w jednym wywołaniu API.

Zapisz dane jako transakcje

Podczas pracy z danymi, które mogą zostać uszkodzone przez równoczesne modyfikacje, takie jak liczniki przyrostowe, można użyć transakcji, przekazując procedurę obsługi transakcji do runTransaction() . Procedura obsługi transakcji przyjmuje bieżący stan danych jako argument i zwraca nowy żądany stan, który chcesz zapisać. Jeśli inny klient zapisuje do lokalizacji przed pomyślnym zapisaniem nowej wartości, funkcja aktualizacji zostanie wywołana ponownie z nową bieżącą wartością i ponowienie próby zapisu.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdkami i usuwanie ich gwiazdką oraz śledzenie liczby gwiazdek, które otrzymał post, w następujący 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 funkcja aktualizacji transakcji jest uruchamiana, więc jeśli uruchomisz funkcję wiele razy, możesz zobaczyć stany pośrednie. Możesz ustawić applyLocally na false , aby pominąć te stany pośrednie i zamiast tego poczekać, aż transakcja zostanie zakończona, zanim zostaną wywołane zdarzenia:

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

Wynikiem transakcji jest TransactionResult , który zawiera takie informacje, 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ę, wywołaj Transaction.abort() , aby zgłosić AbortTransactionException :

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

  // ...
});

print(result.committed); // false

Atomowe przyrosty po stronie serwera

W powyższym przypadku zapisujemy do bazy danych dwie wartości: identyfikator użytkownika, który gwiazdką/odznaczył posta gwiazdką oraz zwiększoną liczbę gwiazdek. Jeśli już wiemy, że użytkownik oznacza post gwiazdką, zamiast transakcji możemy zastosować operację przyrostu atomowego.

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 wykorzystuje operacji transakcyjnej, więc nie jest automatycznie uruchamiany ponownie, jeśli wystąpi konflikt aktualizacji. Ponieważ jednak operacja inkrementacji odbywa się bezpośrednio na serwerze bazy danych, nie ma ryzyka wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, np. oznaczenie użytkownika gwiazdką posta, który już wcześniej dodał, powinieneś napisać niestandardowe reguły bezpieczeństwa dla tego przypadku użycia.

Pracuj z danymi w trybie offline

Jeśli klient utraci połączenie sieciowe, Twoja aplikacja będzie nadal działać poprawnie.

Każdy klient podłączony do bazy danych Firebase utrzymuje własną wewnętrzną wersję wszelkich aktywnych danych. Kiedy dane są zapisywane, są najpierw zapisywane w tej wersji lokalnej. Następnie klient Firebase synchronizuje te dane ze zdalnymi serwerami baz danych i innymi klientami, dokładając wszelkich starań.

W rezultacie wszystkie zapisy do bazy danych natychmiast wyzwalają zdarzenia lokalne, zanim jakiekolwiek dane zostaną zapisane na serwerze. Oznacza to, że Twoja aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub łączności.

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

Więcej o zachowaniu w trybie offline porozmawiamy w artykule Dowiedz się więcej o możliwościach w trybie online i offline .

Następne kroki