Odczyt i zapis danych na Androidzie

W tym dokumencie znajdziesz podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.

Dane Firebase są zapisywane w FirebaseDatabasereferencji i pobierane przez dołączenie do niej asynchronicznego odbiornika. Słuchacz jest wywoływany raz w przypadku początkowego stanu danych i ponownie za każdym razem, gdy dane ulegną zmianie.

(Opcjonalnie) Tworzenie prototypów 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 tworzenia prototypów 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, praca lokalna bez wdrażania usług na żywo może być świetnym pomysłem.

Realtime Database Emulator jest częścią Local Emulator Suite, która umożliwia aplikacji interakcję z emulowaną zawartością bazy danych i konfiguracją, a także opcjonalnie z emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami zabezpieczeń).

Korzystanie z Realtime Database emulatora wymaga wykonania tylko kilku czynności:

  1. Dodanie do konfiguracji testowej aplikacji wiersza kodu, który połączy ją z emulatorem.
  2. Uruchom firebase emulators:start w katalogu głównym projektu lokalnego.
  3. Wykonuj wywołania z kodu prototypu aplikacji za pomocą pakietu Realtime Database SDK platformy lub interfejsu Realtime Database REST API.

Dostępny jest szczegółowy przewodnik dotyczący Realtime DatabaseCloud Functions. Zapoznaj się też z Local Emulator Suite wprowadzeniem.

Pobieranie DatabaseReference

Aby odczytywać lub 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 odwołaniu, zastępując wszystkie istniejące dane w tej ścieżce. Za pomocą tej metody możesz:

  • 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 bez argumentów i publiczne metody pobierania 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 zwiększa też czytelność kodu i ułatwia jego konserwację. 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 z adresem setValue() możesz dodać 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 spowoduje 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 odbiorników

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

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

Metoda onDataChange() służy do odczytywania statycznej migawki zawartości w danej ścieżce w momencie wystąpienia zdarzenia. Ta metoda jest wywoływana raz po dołączeniu odbiornika i ponownie za każdym razem, gdy zmienią się dane, w tym dane dotyczące dzieci. Funkcja zwrotna zdarzenia otrzymuje migawkę zawierającą wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, po wywołaniu funkcji exists()null w przypadku funkcji getValue() migawka zwróci wartość false.

Poniższy przykład pokazuje aplikację do blogowania społecznościowego, która pobiera z bazy danych szczegóły posta:

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 DataSnapshot, który zawiera dane z określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia. Wywołanie getValue() na zrzucie zwraca reprezentację danych w postaci obiektu Java. Jeśli w lokalizacji nie ma danych, wywołanie funkcji getValue() zwraca wartość null.

W tym przykładzie ValueEventListener definiuje też metodę onCancelled(), która jest wywoływana, jeśli odczyt zostanie anulowany. Odczyt może zostać na przykład anulowany, jeśli klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Do tej metody jest przekazywany obiekt DatabaseError wskazujący przyczynę niepowodzenia.

Odczytywanie danych tylko raz

Odczytanie wartości 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.

Ogólnie rzecz biorąc, do odczytywania danych i otrzymywania powiadomień o ich aktualizacjach z backendu należy używać ValueEventListenertechnik opisanych powyżej. Techniki nasłuchiwania zmniejszają zużycie i koszty oraz są zoptymalizowane pod kątem zapewnienia użytkownikom najlepszych wrażeń podczas przechodzenia w tryb online i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć get(), aby uzyskać migawkę danych z bazy danych. Jeśli z jakiegokolwiek powodu get() nie może zwrócić wartości serwera, klient sprawdzi pamięć podręczną lokalnego magazynu i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Niepotrzebne użycie get() może zwiększyć wykorzystanie przepustowości i spowodować utratę wydajności. Można temu zapobiec, używając odbiornika 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 danych tylko raz za pomocą odbiornika

W niektórych przypadkach możesz chcieć, aby wartość z 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 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 funkcji updateChildren() możesz aktualizować wartości podrzędne niższego poziomu, podając ścieżkę do klucza. Jeśli dane są przechowywane w wielu lokalizacjach, aby zapewnić lepszą skalowalność, możesz zaktualizować wszystkie ich wystąpienia za pomocą rozsyłania danych. Na przykład aplikacja do blogowania społecznościowego może mieć klasę Post podobną do tej:

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 kanale ostatnich aktywności i w kanale 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żywamy push(), aby utworzyć post w węźle zawierającym posty wszystkich użytkowników pod adresem /posts/$postid, i jednocześnie pobrać klucz za pomocą getKey(). Klucz może być następnie użyty do utworzenia drugiego wpisu na koncie użytkownika w /user-posts/$userid/$postid.

Korzystając z tych ścieżek, możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania updateChildren(), np. w tym przykładzie, w którym nowy post jest tworzony w obu lokalizacjach. Jednoczesne aktualizacje przeprowadzane w ten sposób są niepodzielne: wszystkie aktualizacje się udają lub wszystkie się nie udają.

Dodawanie wywołania zwrotnego po zakończeniu

Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać odbiornik 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 powiedzie, do odbiorcy zostanie przekazany 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
                // ...
            }
        });

Usuń dane

Najprostszym sposobem usunięcia danych jest wywołanie funkcji removeValue() na odwołaniu do lokalizacji tych danych.

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

Odłączanie detektorów

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

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

Wywołanie removeEventListener() na odbiorniku rodzica nie powoduje automatycznego usunięcia odbiorników zarejestrowanych w jego węzłach podrzędnych. Aby usunąć wywołanie zwrotne, należy też wywołać removeEventListener() na wszystkich odbiornikach podrzędnych.

Zapisywanie danych jako transakcji

W przypadku danych, które mogą zostać uszkodzone przez równoczesne modyfikacje, np. liczników przyrostowych, możesz użyć operacji transakcji. Ta operacja wymaga podania 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 tej lokalizacji, zanim nowa wartość zostanie zapisana, funkcja aktualizacji zostanie ponownie wywołana z nową bieżącą wartością, a zapis zostanie ponowiony.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i cofanie tego oznaczenia oraz śledzić liczbę 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, jeśli kilku użytkowników jednocześnie oznaczy ten sam post gwiazdką lub klient ma nieaktualne dane. Jeśli transakcja zostanie odrzucona, serwer zwróci bieżącą wartość do klienta, który ponownie uruchomi transakcję ze zaktualizowaną wartością. Powtarzaj te czynności, aż transakcja zostanie zaakceptowana lub wykonasz zbyt wiele prób.

Atomowe zwiększanie wartości po stronie serwera

W tym przypadku zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który dodaje lub usuwa gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy, że użytkownik oznaczył posta 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 korzysta z operacji transakcji, więc nie jest automatycznie uruchamiany ponownie w przypadku sprzecznej aktualizacji. Jednak ponieważ operacja zwiększania 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ł, napisz niestandardowe reguły bezpieczeństwa dla tego przypadku użycia.

Praca z danymi offline

Jeśli klient utraci połączenie z siecią, aplikacja będzie nadal działać prawidłowo.

Każdy klient połączony z bazą danych Firebase ma własną wewnętrzną wersję danych, w przypadku których używane są detektory lub które są oznaczone jako dane, które mają być synchronizowane z serwerem. Podczas odczytywania lub zapisywania danych najpierw używana jest ta lokalna wersja danych. Klient Firebase synchronizuje te dane z serwerami zdalnej bazy danych i innymi klientami w miarę możliwości.

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óźnienia sieci lub 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 zachowaniach offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.

Dalsze kroki