Odczyt i zapis danych na Androidzie

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 asynchronicznego detektora. Odbiornik jest aktywowany raz dla stanu początkowego danych i ponownie za każdym razem, gdy dane ulegną zmianie.

(Opcjonalnie) Prototypowanie i testowanie za pomocą Firebase Local Emulator Suite

Zanim omówimy, jak aplikacja odczytuje i zapisuje dane w Realtime Database, przedstawimy zestaw narzędzi, których możesz używać do prototypowania i testowania funkcji Realtime Database: Firebase Local Emulator Suite. Jeśli testujesz różne modele danych, optymalizujesz reguły zabezpieczeń lub szukasz najbardziej opłacalnego sposobu interakcji z backendem, możliwość pracy lokalnej bez wdrażania usług na żywo może być świetnym rozwiązaniem.

Emulator Realtime Database jest częścią Local Emulator Suite, która 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 zabezpieczeń).

Aby użyć emulatora Realtime Database, wystarczy wykonać kilka czynności:

  1. dodanie do konfiguracji testowej aplikacji wiersza kodu, który umożliwia połączenie z emulatorem;
  2. Uruchom firebase emulators:start w katalogu głównym projektu lokalnego.
  3. wykonywanie wywołań z kodu prototypu aplikacji przy użyciu pakietu SDK platformy Realtime Database w zwykły sposób lub przy użyciu interfejsu API typu REST Realtime Database;

Dostępny jest szczegółowy przewodnik dotyczący Realtime Database i Cloud Functions. Warto też zapoznać się z wprowadzeniem do Local Emulator Suite.

Pobieranie DatabaseReference

Aby odczytywać dane z bazy danych lub je w niej zapisywać, 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ć metody setValue(), aby zapisać dane w określonej referencji, zastępując wszystkie istniejące dane w tej ścieżce. Możesz użyć tej metody, aby:

  • Typy kart, które odpowiadają 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 argumentów i ma publiczne metody pobierające 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 zwykle sprawia, ż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;
    }

}

Aby dodać użytkownika za pomocą setValue():

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 metody 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 konieczności ponownego zapisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie profili, możesz zmienić 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ą stałych detektorów

Aby odczytać dane w ścieżce i nasłuchiwać zmian, użyj metody addValueEventListener(), aby dodać ValueEventListener do DatabaseReference.

Słuchacz Wywołanie zwrotne zdarzenia Typowe zastosowanie
ValueEventListener onDataChange() Odczytywanie i nasłuchiwanie zmian w całej zawartości ścieżki.

Możesz użyć metody onDataChange(), aby odczytać statyczny zrzut zawartości w danej ścieżce, który odzwierciedla stan z momentu wystąpienia zdarzenia. Ta metoda jest wywoływana raz, gdy detektor jest dołączony, i ponownie za każdym razem, gdy dane, w tym elementy podrzędne, ulegną zmianie. Do wywołania zwrotnego zdarzenia przekazywany jest zrzut zawierający wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, migawka zwróci false, gdy wywołasz exists(), i null, gdy wywołasz getValue().

Poniższy przykład pokazuje, jak aplikacja do blogowania społecznościowego 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);

Odbiorca otrzymuje obiekt DataSnapshot, który zawiera dane z określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia. Wywołanie metody getValue() na zrzucie zwraca reprezentację danych w postaci obiektu Javy. 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, jeśli klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Do tej metody przekazywany jest obiekt DatabaseError, który wskazuje przyczynę błędu.

Odczytywanie danych raz

Odczytywanie jednorazowe za pomocą metody get()

Pakiet SDK jest przeznaczony do zarządzania interakcjami z serwerami baz danych, niezależnie od tego, czy aplikacja jest online, czy offline.

Zasadniczo do odczytywania danych i otrzymywania powiadomień o aktualizacjach danych z backendu należy używać opisanych powyżej technik ValueEventListener. Techniki detektorów zmniejszają wykorzystanie i opłaty 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 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ą lokalnego miejsca na dane i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Niepotrzebne używanie metody get() może zwiększyć wykorzystanie przepustowości i prowadzić do spadku wydajności, czemu można zapobiec, używając detektora działającego w czasie rzeczywistym, 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()));
        }
    }
});

Odczytywanie jednorazowe za pomocą detektora

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej była zwracana natychmiast, zamiast sprawdzać, czy na serwerze jest zaktualizowana wartość. W takich przypadkach możesz użyć addListenerForSingleValueEvent, aby natychmiast pobrać dane z lokalnej pamięci podręcznej na dysku.

Jest to przydatne w przypadku danych, które muszą zostać wczytane tylko raz i nie powinny często się 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 pisać nowy post.

Aktualizowanie i usuwanie danych

Aktualizowanie określonych pól

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().

Podczas wywoływania updateChildren() możesz aktualizować wartości podrzędne niższego poziomu, określając ścieżkę klucza. Jeśli dane są przechowywane w kilku lokalizacjach, aby zapewnić lepsze skalowanie, możesz zaktualizować wszystkie instancje tych danych za pomocą rozpowszechniania danych. Na przykład aplikacja do blogowania społecznościowego może mieć klasę Post, która wygląda tak:

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ć post i jednocześnie zaktualizować go w karcie Ostatnia aktywność oraz w karcie aktywności użytkownika, który go opublikował, aplikacja do blogowania używa kodu takiego jak ten:

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);
}

W tym przykładzie użyto metody push(), aby utworzyć post w węźle zawierającym posty wszystkich użytkowników w /posts/$postid i jednocześnie pobrać klucz za pomocą metody getKey(). Klucz może być następnie użyty do utworzenia drugiego wpisu w postach użytkownika na stronie /user-posts/$userid/$postid.

Korzystając z tych ścieżek, możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą pojedynczego wywołania updateChildren(), tak jak w tym przykładzie, w którym nowy post jest tworzony w obu lokalizacjach. Jednoczesne aktualizacje wprowadzane w ten sposób są niepodzielne: albo wszystkie aktualizacje się powiodą, albo wszystkie zakończą się niepowodzeniem.

Dodawanie wywołania zwrotnego po zakończeniu

Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać detektor zakończenia. Zarówno setValue(), jak i updateChildren() przyjmują opcjonalny detektor ukończenia, który jest wywoływany, gdy zapis zostanie pomyślnie zatwierdzony w bazie danych. Jeśli wywołanie się nie powiedzie, do detektora zostanie przekazany obiekt błędu z informacją o przyczynie 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
                // ...
            }
        });

Usuń dane

Najprostszym sposobem na usunięcie danych jest wywołanie metody removeValue() w odniesieniu do lokalizacji tych danych.

Możesz też usunąć, podając null jako wartość innej operacji zapisu, np. setValue() lub updateChildren(). Możesz użyć tej techniki z updateChildren(), aby usunąć wiele elementów podrzędnych w jednym wywołaniu interfejsu API.

Odłączanie detektorów

Wywołania zwrotne są usuwane przez wywołanie metody removeEventListener() w odwołaniu do bazy danych Firebase.

Jeśli detektor został dodany do lokalizacji danych kilka razy, jest wywoływany kilka razy w przypadku każdego zdarzenia i musisz go odłączyć tyle samo razy, aby całkowicie go usunąć.

Wywołanie metody removeEventListener() na obiekcie nasłuchującym nadrzędnym nie powoduje automatycznego usunięcia obiektów nasłuchujących zarejestrowanych w jego węzłach podrzędnych. Aby usunąć wywołanie zwrotne, należy wywołać metodę removeEventListener() również na wszystkich obiektach nasłuchujących podrzędnych.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, np. licznikami przyrostowymi, możesz użyć operacji transakcji. Ta operacja wymaga 2 argumentów: funkcji aktualizacji i opcjonalnego wywołania zwrotnego po zakończeniu. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy pożądany stan, który chcesz zapisać. Jeśli inny klient zapisze dane w lokalizacji, zanim nowa wartość zostanie pomyślnie zapisana, funkcja aktualizacji zostanie wywołana ponownie 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 i usuwanie oznaczeń gwiazdką przy postach 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łowemu zliczaniu gwiazdek, gdy wielu użytkowników jednocześnie dodaje gwiazdkę do tego samego posta lub gdy klient ma nieaktualne dane. Jeśli transakcja zostanie odrzucona, serwer zwraca bieżącą wartość do klienta, który ponownie uruchamia transakcję ze zaktualizowaną wartością. Proces ten jest powtarzany, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie przekroczona 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 dodaje post do ulubionych lub usuwa go z ulubionych, oraz zwiększoną liczbę gwiazdek. Jeśli już wiemy, że użytkownik oznaczył post gwiazdką, możemy użyć operacji przyrostu atomowego zamiast transakcji.

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 zostanie automatycznie uruchomiony ponownie, jeśli wystąpi konflikt aktualizacji. Ponieważ jednak operacja inkrementacji odbywa się bezpośrednio na serwerze bazy danych, nie ma możliwości wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty związane z aplikacją, np. gdy użytkownik oznaczy gwiazdką post, który już wcześniej oznaczył gwiazdką, musisz napisać niestandardowe reguły zabezpieczeń dla tego przypadku użycia.

Praca z danymi 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 utrzymuje własną wewnętrzną wersję wszystkich danych, w przypadku których używane są detektory lub które są oznaczone jako synchronizowane z serwerem. Podczas odczytu lub zapisu danych najpierw używana jest ta lokalna wersja danych. Następnie klient Firebase synchronizuje te dane ze zdalnymi serwerami baz 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 aplikacja pozostaje responsywna niezależnie od opóźnień sieci czy 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 zachowaniu offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.

Dalsze kroki