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 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:

  1. Dodajesz wiersz kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
  2. Uruchomienie firebase emulators:start w katalogu głównym projektu lokalnego.
  3. 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.

Dalsze kroki