Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.
Dane Firebase są zapisywane w odniesieniu FirebaseDatabase i pobierane przez dołączenie do niego asynchronicznego detektora. Detektor jest aktywowany raz w przypadku stanu początkowego danych i ponownie za każdym razem, gdy dane się zmienią.
(Opcjonalnie) Tworzenie prototypów i testowanie za pomocą Firebase Local Emulator Suite
Zanim omówimy, jak Twoja aplikacja odczytuje i zapisuje dane w Realtime Database, przedstawimy zestaw narzędzi, których możesz używać do tworzenia prototypów i testowania funkcji Realtime Database: Firebase Local Emulator Suite. 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 Realtime Database jest częścią Local Emulator Suite, 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 bezpieczeństwa).
Korzystanie z emulatora Realtime Database wymaga wykonania kilku czynności:
- 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 funkcji z kodu prototypu aplikacji za pomocą pakietu SDK platformy Realtime Database jak zwykle lub za pomocą interfejsu API REST Realtime Database.
Dostępny jest szczegółowy przewodnik dotyczący Realtime Database i Cloud Functions. Warto też zapoznać się z Local Emulator Suite wprowadzeniem.
Pobieranie DatabaseReference
Aby odczytywać i zapisywać dane w bazie danych, potrzebujesz instancji DatabaseReference:
Kotlin
private lateinit var database: DatabaseReference // ... database = Firebase.database.reference
Java
private DatabaseReference mDatabase; // ... mDatabase = FirebaseDatabase.getInstance().getReference();
Zapisywanie danych
Podstawowe operacje zapisu
W przypadku podstawowych operacji zapisu możesz użyć setValue(), aby zapisać dane w określonym odniesieniu, zastępując wszystkie istniejące dane w tej ścieżce. Za pomocą tej metody możesz:
- Przekazywać typy odpowiadające dostępnym typom JSON w ten sposób:
StringLongDoubleBooleanMap<String, Object>List<Object>
- Przekazywać niestandardowy obiekt Java, jeśli klasa, która go definiuje, ma domyślny konstruktor bez argumentów i publiczne metody pobierające właściwości, które mają zostać przypisane.
Jeśli używasz obiektu Java, jego zawartość jest automatycznie mapowana na lokalizacje podrzędne w sposób zagnieżdżony. Używanie obiektu Java zwykle sprawia też, że kod jest bardziej czytelny i łatwiejszy w utrzymaniu. Jeśli na przykład masz aplikację z podstawowym profilem użytkownika, obiekt User może wyglądać tak:
Kotlin
@IgnoreExtraProperties data class User(val username: String? = null, val email: String? = null) { // Null default values create a no-argument default constructor, which is needed // for deserialization from a DataSnapshot. }
Java
@IgnoreExtraProperties public class User { public String username; public String email; public User() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } public User(String username, String email) { this.username = username; this.email = email; } }
Użytkownika możesz dodać za pomocą setValue() w ten sposób:
Kotlin
fun writeNewUser(userId: String, name: String, email: String) { val user = User(name, email) database.child("users").child(userId).setValue(user) }
Java
public void writeNewUser(String userId, String name, String email) { User user = new User(name, email); mDatabase.child("users").child(userId).setValue(user); }
Użycie setValue() w ten sposób powoduje 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:
Kotlin
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Odczytywanie danych
Odczytywanie danych za pomocą trwałych detektorów
Aby odczytywać dane w ścieżce i nasłuchiwać zmian, użyj addValueEventListener() metody do dodania ValueEventListener do DatabaseReference.
| Detektor | Wywołanie zwrotne zdarzenia | Typowe zastosowanie |
|---|---|---|
ValueEventListener |
onDataChange() |
Odczytywanie i nasłuchiwanie zmian w całej zawartości ścieżki. |
Za pomocą metody onDataChange() możesz odczytać statyczny zrzut zawartości w danej ścieżce w momencie wystąpienia zdarzenia. Ta metoda jest wywoływana raz po dołączeniu detektora i ponownie za każdym razem, gdy zmienią się dane, w tym dane podrzędne. Wywołanie zwrotne zdarzenia otrzymuje zrzut zawierający wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, zrzut zwróci false, gdy wywołasz exists(), i null, gdy wywołasz getValue().
Poniższy przykład pokazuje aplikację do blogowania społecznościowego, która pobiera szczegóły posta z bazy danych:
Kotlin
val postListener = object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { // Get Post object and use the values to update the UI val post = dataSnapshot.getValue<Post>() // ... } override fun onCancelled(databaseError: DatabaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()) } } postReference.addValueEventListener(postListener)
Java
ValueEventListener postListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { // Get Post object and use the values to update the UI Post post = dataSnapshot.getValue(Post.class); // .. } @Override public void onCancelled(DatabaseError databaseError) { // Getting Post failed, log a message Log.w(TAG, "loadPost:onCancelled", databaseError.toException()); } }; mPostReference.addValueEventListener(postListener);
Detektor otrzymuje DataSnapshot, który zawiera dane w określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia. Wywołanie getValue() w zrzucie zwraca reprezentację danych w postaci obiektu Java. Jeśli w lokalizacji nie ma danych, wywołanie getValue() zwraca null.
W tym przykładzie ValueEventListener definiuje też metodę onCancelled(), która jest wywoływana, jeśli odczyt zostanie anulowany. Odczyt może zostać anulowany np. wtedy, gdy klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Ta metoda otrzymuje obiekt DatabaseError wskazujący przyczynę niepowodzenia.
Jednorazowe odczytywanie danych
Jednorazowe odczytywanie za pomocą get()
Pakiet SDK jest zaprojektowany 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 z backendu należy używać opisanych powyżej metod ValueEventListener. Metody detektora zmniejszają zużycie i koszty oraz są zoptymalizowane pod kątem zapewnienia użytkownikom najlepszych wrażeń podczas przechodzenia do trybu online i offline.
Jeśli potrzebujesz danych tylko raz, możesz użyć get(), aby pobrać zrzut 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.
Niepotrzebne używanie get() może zwiększyć zużycie przepustowości i spowodować pogorszenie wydajności. Można temu zapobiec, używając detektora czasu rzeczywistego, jak pokazano powyżej.
Kotlin
mDatabase.child("users").child(userId).get().addOnSuccessListener {
Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
Log.e("firebase", "Error getting data", it)
}
Java
mDatabase.child("users").child(userId).get().addOnCompleteListener(new OnCompleteListener<DataSnapshot>() {
@Override
public void onComplete(@NonNull Task<DataSnapshot> task) {
if (!task.isSuccessful()) {
Log.e("firebase", "Error getting data", task.getException());
}
else {
Log.d("firebase", String.valueOf(task.getResult().getValue()));
}
}
});
Jednorazowe odczytywanie za pomocą detektora
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ć addListenerForSingleValueEvent, 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.
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 updateChildren().
Podczas wywoływania updateChildren(), możesz aktualizować wartości elementów podrzędnych niższego poziomu, określając ścieżkę do klucza. Jeśli dane są przechowywane w wielu lokalizacjach, aby lepiej skalować
, możesz zaktualizować wszystkie instancje tych danych za pomocą
rozsyłania danych. Na przykład aplikacja do blogowania społecznościowego może mieć klasę Post w ten sposób:
Kotlin
@IgnoreExtraProperties data class Post( var uid: String? = "", var author: String? = "", var title: String? = "", var body: String? = "", var starCount: Int = 0, var stars: MutableMap<String, Boolean> = HashMap(), ) { @Exclude fun toMap(): Map<String, Any?> { return mapOf( "uid" to uid, "author" to author, "title" to title, "body" to body, "starCount" to starCount, "stars" to stars, ) } }
Java
@IgnoreExtraProperties public class Post { public String uid; public String author; public String title; public String body; public int starCount = 0; public Map<String, Boolean> stars = new HashMap<>(); public Post() { // Default constructor required for calls to DataSnapshot.getValue(Post.class) } public Post(String uid, String author, String title, String body) { this.uid = uid; this.author = author; this.title = title; this.body = body; } @Exclude public Map<String, Object> toMap() { HashMap<String, Object> result = new HashMap<>(); result.put("uid", uid); result.put("author", author); result.put("title", title); result.put("body", body); result.put("starCount", starCount); result.put("stars", stars); return result; } }
Aby utworzyć posta i jednocześnie zaktualizować go w kanale ostatnich aktywności oraz w kanale aktywności użytkownika, aplikacja do blogowania używa takiego kodu:
Kotlin
private fun writeNewPost(userId: String, username: String, title: String, body: String) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously val key = database.child("posts").push().key if (key == null) { Log.w(TAG, "Couldn't get push key for posts") return } val post = Post(userId, username, title, body) val postValues = post.toMap() val childUpdates = hashMapOf<String, Any>( "/posts/$key" to postValues, "/user-posts/$userId/$key" to postValues, ) database.updateChildren(childUpdates) }
Java
private void writeNewPost(String userId, String username, String title, String body) { // Create new post at /user-posts/$userid/$postid and at // /posts/$postid simultaneously String key = mDatabase.child("posts").push().getKey(); Post post = new Post(userId, username, title, body); Map<String, Object> postValues = post.toMap(); Map<String, Object> childUpdates = new HashMap<>(); childUpdates.put("/posts/" + key, postValues); childUpdates.put("/user-posts/" + userId + "/" + key, postValues); mDatabase.updateChildren(childUpdates); }
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ą getKey(). 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 updateChildren(), 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 zakończenia
Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać detektor zakończenia. Zarówno setValue(), jak i updateChildren() przyjmują opcjonalny detektor zakończenia, który jest wywoływany, gdy zapis zostanie pomyślnie zatwierdzony w bazie danych. Jeśli wywołanie się nie powiodło, detektor otrzymuje obiekt błędu wskazujący przyczynę niepowodzenia.
Kotlin
database.child("users").child(userId).setValue(user) .addOnSuccessListener { // Write was successful! // ... } .addOnFailureListener { // Write failed // ... }
Java
mDatabase.child("users").child(userId).setValue(user) .addOnSuccessListener(new OnSuccessListener<Void>() { @Override public void onSuccess(Void aVoid) { // Write was successful! // ... } }) .addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { // Write failed // ... } });
Usuwanie danych
Najprostszym sposobem usunięcia danych jest wywołanie removeValue() w odniesieniu do lokalizacji tych danych.
Możesz też usunąć dane, określając null jako wartość innej operacji zapisu, np. setValue() lub updateChildren(). Za pomocą tej metody możesz usunąć wiele elementów podrzędnych za pomocą jednego wywołania interfejsu API
za pomocą updateChildren().
Odłączanie detektorów
Wywołania zwrotne są usuwane przez wywołanie metody removeEventListener() w odniesieniu do bazy danych Firebase.
Jeśli detektor został dodany do lokalizacji danych wiele razy, jest wywoływany wielokrotnie dla każdego zdarzenia. Aby go całkowicie usunąć, musisz go odłączyć tyle samo razy.
Wywołanie removeEventListener() w detektorze nadrzędnym nie powoduje automatycznego usunięcia detektorów zarejestrowanych w jego węzłach podrzędnych. Aby usunąć wywołanie zwrotne, należy też wywołać removeEventListener() w detektorach podrzędnych.
Zapisywanie danych jako transakcji
Podczas pracy z danymi, które mogą zostać uszkodzone przez równoczesne modyfikacje, np. liczniki przyrostowe, możesz użyć operacji transakcji. Ta operacja przyjmuje 2 argumenty: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. Funkcja aktualizacji przyjmuje jako argument bieżący stan danych i zwraca nowy stan, który chcesz zapisać. Jeśli inny klient zapisze dane w lokalizacji, zanim nowa wartość zostanie pomyślnie zapisana, funkcja aktualizacji zostanie ponownie wywołana z nową bieżącą wartością, a zapis zostanie ponowiony.
Na przykład w aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i usuwanie gwiazdki oraz śledzenie liczby gwiazdek, które otrzymał post, w ten sposób:
Kotlin
private fun onStarClicked(postRef: DatabaseReference) { // ... postRef.runTransaction(object : Transaction.Handler { override fun doTransaction(mutableData: MutableData): Transaction.Result { val p = mutableData.getValue(Post::class.java) ?: return Transaction.success(mutableData) if (p.stars.containsKey(uid)) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1 p.stars.remove(uid) } else { // Star the post and add self to stars p.starCount = p.starCount + 1 p.stars[uid] = true } // Set value and report transaction success mutableData.value = p return Transaction.success(mutableData) } override fun onComplete( databaseError: DatabaseError?, committed: Boolean, currentData: DataSnapshot?, ) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError!!) } }) }
Java
private void onStarClicked(DatabaseReference postRef) { postRef.runTransaction(new Transaction.Handler() { @NonNull @Override public Transaction.Result doTransaction(@NonNull MutableData mutableData) { Post p = mutableData.getValue(Post.class); if (p == null) { return Transaction.success(mutableData); } if (p.stars.containsKey(getUid())) { // Unstar the post and remove self from stars p.starCount = p.starCount - 1; p.stars.remove(getUid()); } else { // Star the post and add self to stars p.starCount = p.starCount + 1; p.stars.put(getUid(), true); } // Set value and report transaction success mutableData.setValue(p); return Transaction.success(mutableData); } @Override public void onComplete(DatabaseError databaseError, boolean committed, DataSnapshot currentData) { // Transaction completed Log.d(TAG, "postTransaction:onComplete:" + databaseError); } }); }
Użycie transakcji zapobiega nieprawidłowej liczbie gwiazdek, jeśli wielu użytkowników oznaczy ten sam post gwiazdką w tym samym czasie lub jeśli klient miał nieaktualne dane. Jeśli transakcja zostanie odrzucona, serwer zwróci klientowi bieżącą wartość, a klient ponownie uruchomi transakcję ze zaktualizowaną wartością. Powtarza się to, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie wykonana zbyt duża liczba prób.
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ą, zamiast transakcji możemy użyć niepodzielnej operacji przyrostu.
Kotlin
private fun onStarClicked(uid: String, key: String) { val updates: MutableMap<String, Any> = hashMapOf( "posts/$key/stars/$uid" to true, "posts/$key/starCount" to ServerValue.increment(1), "user-posts/$uid/$key/stars/$uid" to true, "user-posts/$uid/$key/starCount" to ServerValue.increment(1), ) database.updateChildren(updates) }
Java
private void onStarClicked(String uid, String key) { Map<String, Object> updates = new HashMap<>(); updates.put("posts/"+key+"/stars/"+uid, true); updates.put("posts/"+key+"/starCount", ServerValue.increment(1)); updates.put("user-posts/"+uid+"/"+key+"/stars/"+uid, true); updates.put("user-posts/"+uid+"/"+key+"/starCount", ServerValue.increment(1)); mDatabase.updateChildren(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 szans na konflikt.
Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, np. gdy użytkownik oznaczy gwiazdką post, 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ą, Twoja aplikacja będzie nadal działać prawidłowo.
Każdy klient połączony z bazą danych Firebase utrzymuje własną wewnętrzną wersję danych, w których są używane detektory lub które są oznaczone jako synchronizowane z serwerem. Gdy dane są odczytywane lub zapisywane, najpierw używana jest ta lokalna wersja danych. Klient Firebase synchronizuje następnie 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 nastąpi jakakolwiek interakcja z serwerem. Oznacza to, że Twoja aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub łączności.
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.