Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.
Dane Firebase są zapisywane w odwołaniu FirebaseDatabase
i pobierane przez dołączenie do niego odbiornika asynchronicznego. Detektor jest aktywowany raz dla początkowego stanu danych i ponownie przy każdej zmianie danych.
(Opcjonalnie) Prototypowanie i testowanie w Pakiecie emulatorów lokalnych Firebase
Zanim porozmawiamy o tym, jak Twoja aplikacja odczytuje dane w Bazie danych czasu rzeczywistego i zapisuje je w tej bazie, omówię zestaw narzędzi, których możesz użyć do prototypowania i testowania funkcji Bazy danych czasu rzeczywistego: Pakiet emulatorów lokalnych Firebase. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły zabezpieczeń lub szukasz najbardziej ekonomicznego sposobu interakcji z backendem, możliwość pracy lokalnej bez wdrażania aktywnych usług może być świetnym pomysłem.
Emulator bazy danych czasu rzeczywistego jest częścią Pakietu emulatorów lokalnych, który umożliwia aplikacji interakcję z treścią i konfiguracją emulowanej bazy danych, a także opcjonalnie emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami zabezpieczeń).
Aby użyć emulatora Bazy danych czasu rzeczywistego, wystarczy kilka kroków:
- Dodajesz wiersz kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
- Uruchomienie
firebase emulators:start
w katalogu głównym projektu lokalnego. - Wywoływanie z prototypowego kodu aplikacji za pomocą pakietu SDK platformy Baza danych czasu rzeczywistego lub interfejsu API REST Bazy danych czasu rzeczywistego.
Dostępny jest szczegółowy instrukcja korzystania z Bazy danych czasu rzeczywistego i Cloud Functions. Zapoznaj się też z wprowadzeniem do Pakietu emulatorów lokalnych.
Pobieranie odniesienia do bazy danych
Aby móc odczytywać lub zapisywać dane z bazy danych, potrzebujesz instancji DatabaseReference
:
Kotlin+KTX
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ć polecenia setValue()
, aby zapisać dane w określonym odwołaniu, zastępując wszystkie dotychczasowe dane w tej ścieżce. Możesz użyć tej metody, aby:
- Typy kart odpowiadające dostępnym typom JSON:
String
Long
Double
Boolean
Map<String, Object>
List<Object>
- Przekaż niestandardowy obiekt Java, jeśli klasa, która go definiuje, ma domyślny konstruktor, który nie przyjmuje żadnych argumentów i ma publiczne metody pobierania dla 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życie obiektu Java zazwyczaj zwiększa też
czytelność kodu i utrzymanie w jego utrzymaniu. Jeśli na przykład masz aplikację z podstawowym profilem użytkownika, obiekt User
może wyglądać tak:
Kotlin+KTX
@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 setValue()
możesz dodać w ten sposób:
Kotlin+KTX
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 tego parametru setValue()
spowoduje zastąpienie danych w określonej lokalizacji, w tym wszystkich węzłów podrzędnych. Mimo to można zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie swoich profili, możesz zmienić nazwę użytkownika w ten sposób:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Odczyt danych
Odczytywanie danych za pomocą trwałych detektorów
Aby odczytywać dane na ścieżce i nasłuchiwać zmian, dodaj ValueEventListener
do DatabaseReference
za pomocą metody addValueEventListener()
.
Detektor | Wywołanie zwrotne zdarzenia | Typowe zastosowanie |
---|---|---|
ValueEventListener |
onDataChange() |
Odczyt i nasłuchiwanie zmian w całej zawartości ścieżki. |
Metoda onDataChange()
pozwala odczytać statyczny zrzut zawartości w danej ścieżce, która istniała w momencie wystąpienia zdarzenia. Metoda ta jest wyzwalana raz po podłączeniu detektora i ponownie za każdym razem, gdy dane, w tym elementy podrzędne, zostaną zmienione. Wywołanie zwrotne zdarzenia jest przekazywane do zrzutu zawierającego wszystkie dane w danej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, zrzut zwróci wartość false
, gdy wywołasz exists()
i null
przy wywołaniu z nim getValue()
.
Ten przykład przedstawia aplikację do obsługi blogów społecznościowych, która pobiera szczegóły posta z bazy danych:
Kotlin+KTX
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 obiekt DataSnapshot
zawierający dane z określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia. Wywołanie getValue()
w zrzucie zwraca dane w obiekcie Java. Jeśli w danej lokalizacji nie ma danych, wywołanie funkcji getValue()
zwróci wartość null
.
W tym przykładzie ValueEventListener
określa też metodę onCancelled()
, która jest wywoływana po anulowaniu odczytu. Na przykład odczyt może zostać anulowany, jeśli klient nie ma uprawnień do odczytu lokalizacji bazy danych Firebase. Ta metoda jest przekazywana do obiektu DatabaseError
, który wskazuje przyczynę błędu.
Odczytaj dane raz
Odczytaj raz za pomocą get()
Pakiet SDK służy do zarządzania interakcjami z serwerami baz danych niezależnie od tego, czy aplikacja działa 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 ValueEventListener
. Stosowane przez nas techniki detektora zmniejszają wykorzystanie zasobów i płatności oraz są zoptymalizowane pod kątem maksymalnej wygody użytkowników korzystających z internetu i offline.
Jeśli dane są potrzebne tylko raz, możesz użyć funkcji get()
, aby uzyskać zrzut danych z bazy danych. Jeśli z jakiegoś powodu get()
nie może zwrócić wartości serwera, klient sprawdzi pamięć podręczną pamięci lokalnej i jeśli nadal nie znajdzie wartości, zwróci błąd.
Niepotrzebne użycie get()
może zwiększyć wykorzystanie przepustowości i spowodować utratę wydajności. Można temu zapobiec, korzystając z detektora w czasie rzeczywistym (jak pokazano powyżej).
Kotlin+KTX
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()));
}
}
});
Czytanie raz za pomocą detektora
W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej była zwracana natychmiast, zamiast sprawdzania aktualizacji na serwerze. W takich przypadkach możesz użyć polecenia addListenerForSingleValueEvent
, aby od razu pobrać dane z pamięci podręcznej dysku lokalnego.
Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i nie będą się często zmieniać ani wymagać aktywnego słuchania. Na przykład aplikacja do blogowania z poprzednich przykładów używa tej metody do wczytywania profilu użytkownika, gdy zaczyna pisać nowy post.
Aktualizowanie lub usuwanie danych
Zaktualizuj określone pola
Aby jednocześnie zapisywać dane w określonych węzłach podrzędnych węzła bez zastępowania innych węzłów podrzędnych, użyj metody updateChildren()
.
Gdy wywołujesz updateChildren()
, możesz zaktualizować wartości podrzędne niższego poziomu, podając ścieżkę klucza. Jeśli dane są przechowywane w kilku lokalizacjach w celu zwiększenia ich skalowania, możesz zaktualizować wszystkie ich wystąpienia za pomocą przekazywania danych na zewnątrz. Na przykład aplikacja do obsługi blogów społecznościowych może mieć klasę Post
taką:
Kotlin+KTX
@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 do kanału ostatniej aktywności oraz kanału aktywności użytkownika publikującego, aplikacja do tworzenia blogów używa takiego kodu:
Kotlin+KTX
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); }
W tym przykładzie użyto metody push()
do utworzenia w węźle posta z postami dla wszystkich użytkowników w domenie /posts/$postid
oraz jednoczesnego pobrania klucza za pomocą funkcji getKey()
. Klucza można następnie użyć do utworzenia drugiego wpisu w postach użytkownika pod adresem /user-posts/$userid/$postid
.
Za pomocą tych ścieżek możesz aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania updateChildren()
. Tak jak w tym przykładzie, w obu lokalizacjach. Aktualizacje wprowadzane w ten sposób
są niepodzielne: albo wszystkie aktualizacje zakończą się sukcesem, albo wszystkie się kończą.
Dodaj zakończone wywołanie zwrotne
Jeśli chcesz się dowiedzieć, kiedy dane zostały zatwierdzone, możesz dodać detektor uzupełnienia. Zarówno setValue()
, jak i updateChildren()
przyjmują opcjonalny detektor ukończenia, który jest wywoływany po pomyślnym zatwierdzeniu zapisu w bazie danych. Jeśli wywołanie się nie udało, detektor przekazuje obiekt błędu z informacją o przyczynie tego błędu.
Kotlin+KTX
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 // ... } });
Usuń dane
Najprostszym sposobem usunięcia danych jest wywołanie metody removeValue()
w odniesieniu do lokalizacji tych danych.
Możesz też usunąć plik, określając null
jako wartość innej operacji zapisu, np. setValue()
lub updateChildren()
. Możesz używać tej metody razem z funkcją updateChildren()
, aby usunąć wiele elementów podrzędnych w jednym wywołaniu interfejsu API.
Odłącz detektory
Wywołania zwrotne są usuwane poprzez wywołanie metody removeEventListener()
w odniesieniu do bazy danych Firebase.
Jeśli detektor został dodany do lokalizacji danych wiele razy, jest wywoływany wiele razy w przypadku każdego zdarzenia. Aby całkowicie usunąć odbiornik, musisz go odłączyć tyle samo razy.
Wywołanie removeEventListener()
w detektorze nadrzędnym nie powoduje automatycznego usunięcia detektorów zarejestrowanych w węzłach podrzędnych. Aby usunąć wywołanie zwrotne, funkcja removeEventListener()
musi też zostać wywołana we wszystkich detektorach podrzędnych.
Zapisywanie danych jako transakcji
Podczas pracy z danymi, które mogą ulec uszkodzeniu w wyniku równoczesnych modyfikacji, takich jak liczniki przyrostowe, możesz użyć operacji transakcji. Przyznajesz tej operacji 2 argumenty: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy pożądany stan do zapisania. Jeśli inny klient zapisze dane w lokalizacji, zanim Twoja nowa wartość zostanie zapisana, funkcja aktualizacji zostanie wywołana ponownie z nową bieżącą wartością, a zapis jest ponawiany.
Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz umożliwić użytkownikom oznaczanie postów gwiazdką i usuwanie gwiazdek oraz śledzenie liczby przyznanych gwiazdek:
Kotlin+KTX
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ło gwiazdką ten sam post w tym samym czasie lub klient miał nieaktualne dane. W przypadku odrzucenia transakcji serwer zwraca bieżącą wartość klientowi, który uruchamia transakcję ponownie ze zaktualizowaną wartością. Powtarza się, dopóki transakcja nie zostanie zaakceptowana lub nie dojdzie do zbyt wielu prób.
Atomic przyrosty po stronie serwera
W powyższym przypadku użycia zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który postawił gwiazdką lub usunął oznaczenie gwiazdką, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznacza posta gwiazdką, możemy użyć niepodzielnej operacji przyrostu zamiast transakcji.
Kotlin+KTX
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 uruchamiany ponownie w przypadku wystąpienia konfliktu aktualizacji. Ponieważ jednak operacja zwiększania odbywa się bezpośrednio na serwerze bazy danych, nie ma ryzyka konfliktu.
Jeśli chcesz wykrywać i odrzucać konflikty dotyczące aplikacji (np. gdy użytkownik oznacza posta gwiazdką już wcześniej), musisz utworzyć niestandardowe reguły zabezpieczeń odpowiednie do tego zastosowania.
Praca z danymi w trybie offline
Jeśli klient utraci połączenie sieciowe, aplikacja będzie nadal działać prawidłowo.
Każdy klient połączony z bazą danych Firebase zachowuje własną, wewnętrzną wersję wszystkich danych, których detektory są używane lub które są oznaczone jako synchronizowane z serwerem. Podczas odczytywania lub zapisywania danych najpierw używana jest ta lokalna wersja danych. Klient Firebase synchronizuje te dane z serwerami zdalnych baz danych oraz z innymi klientami w miarę swoich możliwości.
W rezultacie wszystkie zapisy w bazie danych wywołują zdarzenia lokalne natychmiast, przed jakąkolwiek interakcją z serwerem. Oznacza to, że aplikacja pozostaje responsywna niezależnie od opóźnień sieciowych i połączeń.
Gdy połączenie zostanie przywrócone, aplikacja otrzyma odpowiedni zestaw zdarzeń, aby klient mógł synchronizować się z obecnym stanem serwera bez konieczności pisania niestandardowego kodu.
Więcej informacji o zachowaniu w trybie offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.