(Opcjonalnie) Prototypowanie i testowanie za pomocą Pakietu emulatorów Firebase
Zanim omówimy, jak aplikacja odczytuje i zapisuje dane w Bazie danych czasu rzeczywistego, 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 bezpieczeństwa lub szukasz najbardziej opłacalnego sposobu interakcji z backendem, praca lokalna bez wdrażania usług na żywo może być dobrym rozwiązaniem.
Emulator Bazy danych czasu rzeczywistego jest częścią Pakietu emulatorów, który umożliwia aplikacji interakcję z emulowaną treścią i konfiguracją 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:
- Dodanie wiersza kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
- Uruchomienie polecenia
firebase emulators:startw katalogu głównym projektu lokalnego. - Wywoływanie kodu prototypu aplikacji za pomocą pakietu SDK platformy Bazy danych czasu rzeczywistego lub interfejsu REST API Bazy danych czasu rzeczywistego.
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 DatabaseReference
Aby odczytywać i zapisywać dane w bazie danych, potrzebujesz instancji DatabaseReference:
DatabaseReference ref = FirebaseDatabase.instance.ref();
Zapisywanie danych
Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.
Dane Firebase są zapisywane w DatabaseReference i pobierane przez oczekiwanie na zdarzenia emitowane przez odniesienie lub nasłuchiwanie ich. 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 odniesieniu, zastępując wszystkie istniejące dane w tej ścieżce. Możesz ustawić odniesienie do tych typów: String, boolean, int, double, Map, List.
Możesz na przykład dodać użytkownika za pomocą 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 ponownego zapisywania całego obiektu. Jeśli chcesz umożliwić użytkownikom 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 jednoczesne aktualizowanie 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",
});
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 elementu DatabaseReference, aby nasłuchiwać DatabaseEvent.
Możesz użyć DatabaseEvent, aby odczytać dane w danej ścieżce w momencie zdarzenia. To zdarzenie jest wywoływane raz po dołączeniu detektora i ponownie za każdym razem, gdy zmienią się dane, w tym dane podrzędne. Zdarzenie ma właściwość snapshot, która zawiera wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, właściwość exists migawki będzie mieć wartość false, a właściwość value będzie mieć wartość null.
Poniższy przykład pokazuje aplikację do blogowania społecznościowego, która pobiera szczegóły posta z bazy danych:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Detektor otrzymuje DataSnapshot, który zawiera dane w określonej lokalizacji w bazie danych w momencie zdarzenia we właściwości value.
Odczytywanie danych jednorazowo
Odczytywanie jednorazowe za pomocą 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 należy używać opisanych powyżej technik zdarzeń wartości, aby otrzymywać powiadomienia o aktualizacjach danych z backendu. Te techniki zmniejszają zużycie i opłaty oraz są zoptymalizowane pod kątem zapewnienia użytkownikom jak najlepszych wrażeń podczas przechodzenia do trybu online i offline.
Jeśli potrzebujesz danych tylko raz, możesz użyć get(), aby pobrać migawkę danych z bazy danych. Jeśli z jakiegoś powodu 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 pokazuje jednorazowe pobieranie 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 get() może zwiększyć zużycie przepustowości i spowodować spadek wydajności, czemu można zapobiec, używając detektora w czasie rzeczywistym, jak pokazano powyżej.
Odczytywanie danych jednorazowo za pomocą once()
W niektórych przypadkach możesz chcieć, aby wartość z lokalnej 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 tworzyć 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 elementach podrzędnych węzła bez zastępowania innych węzłów podrzędnych, użyj metody update().
Podczas wywoływania update() możesz aktualizować wartości elementów podrzędnych niższego poziomu, określając ścieżkę klucza. Jeśli dane są przechowywane w wielu lokalizacjach, aby lepiej skalować
, 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 oraz w kanale aktywności użytkownika publikującego. Aby to zrobić, aplikacja do blogowania używa kodu takiego jak ten:
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);
}
Ten przykład używa push() do utworzenia posta w węźle zawierającym posty wszystkich użytkowników w /posts/$postid i jednocześnie pobiera klucz za pomocą key. Klucz można następnie użyć do utworzenia drugiego wpisu w postach użytkownika w /user-posts/$userid/$postid.
Za pomocą tych ścieżek możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania update(), tak jak w tym przykładzie, który tworzy nowy post w obu lokalizacjach. Jednoczesne aktualizacje wykonane w ten sposób są niepodzielne: wszystkie aktualizacje się powiodą lub wszystkie się nie powiodą.
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ą Future, do których możesz dołączyć wywołania zwrotne sukcesu i błędu, które są wywoływane, gdy zapis zostanie zatwierdzony w bazie danych i gdy wywołanie się nie powiedzie.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Usuwanie danych
Najprostszym sposobem usunięcia danych jest wywołanie remove() w odniesieniu do lokalizacji tych danych.
Możesz też usunąć dane, określając wartość null jako wartość innej operacji zapisu, takiej jak set() lub update(). Możesz użyć tej techniki z update(), aby usunąć wiele elementów podrzędnych za pomocą 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
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 zapisze dane w 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 umożliwić użytkownikom oznaczanie postów gwiazdką i usuwanie gwiazdki oraz śledzenie liczby gwiazdek otrzymanych przez 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, 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 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 nowa migawka:
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
Niepodzielne przyrosty po stronie serwera
W powyższym przypadku użycia zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który oznaczył post gwiazdką lub usunął gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznaczył post gwiazdką, możemy użyć operacji niepodzielnego przyrostu 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 używa operacji transakcji, więc nie jest automatycznie ponownie uruchamiany w przypadku konfliktu aktualizacji. Ponieważ jednak operacja przyrostu odbywa się bezpośrednio na serwerze bazy danych, nie ma ryzyka konfliktu.
Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, np. gdy użytkownik oznaczy post gwiazdką, który już wcześniej oznaczył, musisz napisać 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 utrzymuje własną wewnętrzną wersję aktywnych danych. Gdy dane są zapisywane, najpierw są zapisywane w tej lokalnej wersji. Klient Firebase synchronizuje te dane z serwerami zdalnej bazy danych i innymi klientami na zasadzie „najlepszych starań”.
W rezultacie wszystkie zapisy w bazie danych natychmiast wywołują zdarzenia lokalne, 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 działaniu offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.