W tym dokumencie opisano podstawy odczytywania i zapisywania danych Firebase.
Dane Firebase są zapisywane w odwołaniu FirebaseDatabase
i pobierane przez dołączenie asynchronicznego odbiornika do odniesienia. Odbiornik jest uruchamiany raz dla stanu początkowego danych i ponownie za każdym razem, gdy dane się zmieniają.
(Opcjonalnie) Stwórz prototyp i przetestuj za pomocą pakietu Firebase Local Emulator Suite
Zanim porozmawiamy o tym, jak Twoja aplikacja odczytuje i zapisuje w bazie danych Realtime Database, przedstawmy zestaw narzędzi, których możesz użyć do prototypowania i testowania funkcjonalności bazy danych Realtime: Firebase Local 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 Local 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).
Korzystanie z emulatora bazy danych czasu rzeczywistego obejmuje tylko kilka kroków:
- Dodanie wiersza kodu do konfiguracji testowej aplikacji w celu nawiązania połączenia z emulatorem.
- Z katalogu głównego lokalnego katalogu projektu uruchom
firebase emulators:start
. - 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 Local Emulator Suite .
Uzyskaj odniesienie do bazy danych
Aby odczytać lub zapisać 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();
Zapisz dane
Podstawowe operacje zapisu
W przypadku podstawowych operacji zapisu można użyć setValue()
w celu zapisania danych w określonym odwołaniu, zastępując wszelkie istniejące dane w tej ścieżce. Możesz użyć tej metody, aby:
- Typy przebiegów odpowiadające dostępnym typom JSON w następujący sposób:
-
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 argumentów i ma publiczne moduły pobierające dla przypisanych właściwości.
Jeśli używasz obiektu Java, zawartość obiektu jest automatycznie mapowana do lokalizacji podrzędnych w sposób zagnieżdżony. Korzystanie z obiektu Java zazwyczaj sprawia, że kod jest bardziej czytelny i łatwiejszy w utrzymaniu. Na przykład, jeśli masz aplikację z podstawowym profilem użytkownika, obiekt User
może wyglądać następująco:
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; } }
Możesz dodać użytkownika za pomocą setValue()
w następujący 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 metody setValue()
w ten sposób powoduje nadpisanie danych w określonej lokalizacji, łącznie ze wszystkimi węzłami podrzędnymi. 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:
Kotlin+KTX
database.child("users").child(userId).child("username").setValue(name)
Java
mDatabase.child("users").child(userId).child("username").setValue(name);
Przeczytaj dane
Odczytuj dane za pomocą stałych słuchaczy
Aby odczytać dane w ścieżce i nasłuchiwać zmian, użyj metody addValueEventListener()
w celu dodania ValueEventListener
do DatabaseReference
.
Słuchacz | Odwołanie zdarzenia | Typowe użycie |
---|---|---|
ValueEventListener | onDataChange() | Czytaj i słuchaj zmian w całej zawartości ścieżki. |
Za pomocą metody onDataChange()
można odczytać statyczną migawkę zawartości danej ścieżki w stanie, w jakim istniała w momencie zdarzenia. Ta metoda jest uruchamiana raz, gdy słuchacz jest podłączony, i ponownie za każdym razem, gdy zmieniają się dane, w tym dzieci. Do wywołania zwrotnego zdarzenia przekazywana jest migawka zawierająca wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, zrzut zwróci false
, gdy wywołasz funkcję exists()
i null
, gdy wywołasz na niej getValue()
.
Poniższy przykład ilustruje aplikację do blogowania społecznościowego pobierającą szczegóły wpisu 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);
Odbiornik otrzymuje DataSnapshot
, który zawiera dane w określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia. Wywołanie funkcji getValue()
na migawce zwraca obiektową reprezentację danych w języku Java. Jeśli w lokalizacji nie ma żadnych danych, wywołanie metody getValue()
zwraca null
.
W tym przykładzie ValueEventListener
definiuje także metodę onCancelled()
, która jest wywoływana w przypadku anulowania odczytu. Na przykład odczyt można anulować, jeśli klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Do tej metody przekazywany jest obiekt DatabaseError
wskazujący przyczynę wystąpienia błędu.
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 ValueEventListener
do odczytywania danych i otrzymywania powiadomień o aktualizacjach danych z zaplecza. Techniki nasłuchiwania zmniejszają wykorzystanie i rozliczenia oraz są zoptymalizowane, aby zapewnić użytkownikom najlepsze doświadczenia w trybie online 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.
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.
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()));
}
}
});
Przeczytaj raz, korzystając ze słuchacza
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ć addListenerForSingleValueEvent
, 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 wpisu.
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 updateChildren()
.
Wywołując funkcję updateChildren()
, 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 mieć następującą klasę Post
:
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ć post i jednocześnie zaktualizować go do kanału ostatniej aktywności i kanału aktywności użytkownika zamieszczającego post, aplikacja do blogowania używa następującego 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 zastosowano metodę push()
do utworzenia wpisu w węźle zawierającym posty dla wszystkich użytkowników w /posts/$postid
i jednoczesnego pobrania klucza za pomocą getKey()
. 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 updateChildren()
, 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 dodać słuchacza zakończenia. Zarówno setValue()
jak i updateChildren()
pobierają opcjonalny odbiornik uzupełniania, który jest wywoływany, gdy zapis został pomyślnie zatwierdzony w bazie danych. Jeśli wywołanie nie powiedzie się, do słuchacza zostanie przekazany obiekt błędu wskazujący przyczynę niepowodzenia.
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 // ... } });
Usunąć dane
Najprostszym sposobem usunięcia danych jest wywołanie metody removeValue()
w odniesieniu do lokalizacji tych danych.
Można również usunąć, podając null
jako wartość innej operacji zapisu, takiej jak setValue()
lub updateChildren()
. Możesz użyć tej techniki z updateChildren()
, aby usunąć wiele dzieci w jednym wywołaniu API.
Odłącz słuchaczy
Wywołania zwrotne są usuwane poprzez wywołanie metody removeEventListener()
w odwołaniu do bazy danych Firebase.
Jeśli słuchacz został dodany do lokalizacji danych wiele razy, jest on wywoływany wielokrotnie dla każdego zdarzenia i należy go odłączyć tyle samo razy, aby go całkowicie usunąć.
Wywołanie metody removeEventListener()
na odbiorniku nadrzędnym nie powoduje automatycznego usunięcia odbiorników zarejestrowanych w jego węzłach podrzędnych; removeEventListener()
należy także wywołać na wszystkich odbiornikach podrzędnych, aby usunąć wywołanie zwrotne.
Zapisz dane jako transakcje
Podczas pracy z danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, takie jak liczniki przyrostowe, można użyć operacji transakcyjnej . Podajesz tej operacji dwa argumenty: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. Funkcja aktualizacji 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:
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łowemu zliczeniu gwiazdek, jeśli wielu użytkowników oznacza ten sam post w tym samym czasie lub klient ma nieaktualne dane. Jeżeli transakcja zostanie odrzucona, serwer zwraca aktualną wartość klientowi, który ponownie uruchamia transakcję ze zaktualizowaną wartością. Powtarza się to do momentu zaakceptowania transakcji lub podjęcia zbyt wielu prób.
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.
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 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 danych, w których wykorzystywane są odbiorniki lub które są oznaczone w celu synchronizacji z serwerem. Podczas odczytywania lub zapisywania danych w pierwszej kolejności używana jest lokalna wersja danych. 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 wyzwalają lokalne zdarzenia natychmiast, przed jakąkolwiek interakcją 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 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 .