Odczyt i zapis danych na Androidzie

Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.

Dane Firebase są zapisywane w pliku referencyjnym FirebaseDatabase i odbierane przez dołączenie do niego asynchronicznego odbiornika. Listener jest uruchamiany raz w przypadku początkowego stanu danych i po raz kolejny, gdy dane ulegną zmianie.

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

Zanim omówimy sposób odczytu i zapisu danych do Realtime Database przez aplikację, zapoznamy Cię z zestawem narzędzi, które możesz wykorzystać do tworzenia prototypów i testowania funkcji Realtime Database: Firebase Local Emulator Suite. Jeśli testujesz różne modele danych, optymalizujesz reguły bezpieczeństwa lub szukasz najbardziej opłacalnego sposobu interakcji z back-endem, warto pracować lokalnie bez wdrażania usług na żywo.

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

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

  1. Dodanie linii kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
  2. W katalogu głównym lokalnego katalogu projektu uruchom firebase emulators:start.
  3. Wykonywanie wywołań z prototypowego kodu aplikacji za pomocą pakietu SDK platformy Realtime Database w zwykły sposób lub za pomocą interfejsu API REST Realtime Database.

Dostępny jest szczegółowy samouczek dotyczący funkcji Realtime DatabaseCloud Functions. Zapoznaj się też z Local Emulator Suitewprowadzeniem.

Pobieranie obiektu DatabaseReference

Aby odczytywać lub zapisywać dane w bazie danych, musisz mieć instancję 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ć funkcji setValue(), aby zapisać dane w określonej referencji, zastępując wszystkie istniejące dane na tej ścieżce. Dzięki tej metodzie możesz:

  • Typy przepustki odpowiadające dostępnym typom danych 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 gettera dla właściwości, które mają zostać przypisane.

Jeśli używasz obiektu Java, zawartość obiektu jest automatycznie mapowana do podrzędnych lokalizacji w postaci zagnieżdżonej. Korzystanie z obiektu Java zwykle ułatwia też czytelność i utrzymanie kodu. 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;
    }

}

Aby dodać użytkownika z uprawnieniami setValue(), wykonaj te czynności:

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 w ten sposób wartości setValue() spowoduje zastąpienie danych w wybranej lokalizacji, w tym wszystkich węzłów podrzędnych. Nadal jednak możesz zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie ich profili, możesz zaktualizować 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);

Odczytywanie danych

Odczytywanie danych za pomocą stałych odbiorców

Aby odczytywać dane na ścieżce i monitorować zmiany, użyj metody addValueEventListener(), aby dodać ValueEventListener do DatabaseReference.

Listener Połączenie zwrotne zdarzenia Typowe zastosowanie
ValueEventListener onDataChange() czytać i słuchać zmian w całej zawartości ścieżki;

Metoda onDataChange() umożliwia odczyt statycznego migawka zawartości w danej ścieżce w stanie, w jakim znajdowała się ona w momencie zdarzenia. Ta metoda jest wywoływana raz, gdy słuchacz jest dołączony, i ponownie za każdym razem, gdy dane, w tym dzieci, ulegną zmianie. Funkcja wywołania z powrotu zdarzenia otrzymuje migawkę zawierającą wszystkie dane w danej lokalizacji, w tym dane podrzędne. Jeśli nie ma żadnych danych, snapshot zwróci wartość false, gdy wywołasz exists(), i null, gdy wywołasz getValue().

Ten przykład pokazuje aplikację do blogowania społecznościowego, która pobiera 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);

Listener otrzymuje DataSnapshot, który zawiera dane z określonej lokalizacji w bazie danych w momencie zdarzenia. Wywołanie metody getValue() w przypadku snapshot zwraca obiekt Java reprezentujący dane. Jeśli w danej lokalizacji nie ma żadnych danych, wywołanie funkcji getValue() zwraca null.

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

odczytywanie danych raz;

Jednokrotny odczyt za pomocą get()

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

Ogólnie zalecamy używanie opisanych powyżej technik ValueEventListener do odczytywania danych, aby otrzymywać powiadomienia o aktualizacjach danych z back-endu. Techniki związane z odsłuchiwaniem zmniejszają wykorzystanie i obciążenia, a także są optymalizowane pod kątem zapewnienia użytkownikom jak najlepszych wrażeń w sieci i poza nią.

Jeśli dane są potrzebne tylko raz, możesz użyć funkcji get(), aby uzyskać podsumowanie 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żywanie funkcji get() może zwiększyć wykorzystanie przepustowości i spowodować spadek wydajności. Można temu zapobiec, używając odbiorcy 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ą listenera

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

Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i które nie powinny się często zmieniać ani wymagać aktywnego słuchania. Na przykład aplikacja do blogowania w poprzednich przykładach używa tej metody do wczytania profilu użytkownika, gdy ten zacznie pisać nowy post.

Aktualizowanie i usuwanie danych

Aktualizowanie konkretnych pól

Aby jednocześnie zapisywać dane w określonych węzłach podrzędnych bez nadpisywania 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ę klucza. Jeśli dane są przechowywane w kilku lokalizacjach, aby zapewnić lepszą skalowalność, możesz zaktualizować wszystkie wystąpienia tych danych za pomocą rozgałęzienia danych. Na przykład aplikacja do blogowania społecznościowego może mieć klasę Post, która wygląda 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ć post i jednocześnie zaktualizować go w strumieniach aktywności użytkownika i publikującego, aplikacja do blogowania używa kodu podobnego do tego:

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 funkcja push() służy do tworzenia postu w węźle zawierającym posty wszystkich użytkowników w węźle /posts/$postid i jednoczesnego pobierania klucza za pomocą funkcji getKey(). Klucz można następnie wykorzystać do utworzenia drugiego wpisu w postach użytkownika na stronie /user-posts/$userid/$postid.

Dzięki tym ścieżkom możesz przeprowadzać jednoczesne aktualizacje wielu lokalizacji w drzewie JSON za pomocą jednego wywołania funkcji updateChildren(), jak w tym przykładzie, w którym tworzy nowy post w obu lokalizacjach. Jednoczesne aktualizacje w ten sposób są atomowe: albo wszystkie się udają, albo wszystkie się nie udają.

Dodawanie wywołania zwrotnego po zakończeniu

Jeśli chcesz wiedzieć, kiedy dane zostały zapisane, możesz dodać listenera zakończenia. Zarówno setValue(), jak i updateChildren() przyjmują opcjonalny detektor zakończenia, który jest wywoływany, gdy zapisanie danych do bazy danych zostało pomyślnie zaakceptowane. Jeśli wywołanie zakończyło się niepowodzeniem, odbiorca otrzyma obiekt błędu wskazujący, dlaczego wystąpił błąd.

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 usuwania danych jest wywołanie funkcji removeValue() z odniesieniem do lokalizacji tych danych.

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

Odłączanie słuchaczy

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

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

Wywołanie metody removeEventListener() na odbiorcy nadrzędnym nie powoduje automatycznego usuwania odbiorców zarejestrowanych na jego węzłach podrzędnych. Aby usunąć wywołania zwrotne, należy też wywołać metodę removeEventListener() na wszystkich odbiorcach podrzędnych.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą zostać uszkodzone przez równoległe modyfikacje, np. z licznikami przyrostowymi, możesz użyć operacji transakcji. Ta operacja wymaga podania 2 argumentów: funkcji aktualizacji i opcjonalnego wywołania zwrotnego po zakończeniu. Funkcja update przyjmuje jako argument bieżący stan danych i zwraca nowy pożądany stan, który chcesz zapisać. Jeśli inny klient zapisze w tej lokalizacji dane, zanim uda się zapisać nową wartość, funkcja aktualizacji zostanie wywołana ponownie z nową bieżącą wartością i ponownie spróbuje zapisać dane.

Na przykład w przypadku aplikacji do prowadzenia bloga społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i usuwanie tej oceny oraz śledzenie, ile gwiazdek otrzymał dany post. W tym celu:

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

Transakcja zapobiega błędnym wartościom liczby gwiazdek, jeśli więcej użytkowników oznaczy gwiazdką ten sam post w tym samym czasie lub jeśli klient ma nieaktualne dane. Jeśli transakcja zostanie odrzucona, serwer zwróci bieżącą wartość klientowi, który ponownie wykona transakcję z aktualną wartością. Czynność ta jest powtarzana, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie podjęta zbyt duża liczba prób.

Atomowe zwiększenia po stronie serwera

W tym przypadku do bazy danych zapisujemy 2 wartości: identyfikator użytkownika, który oznaczył post gwiazdką lub odznaczył go z gwiazdką, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy, że użytkownik oznaczył post jako ulubiony, możemy użyć operacji atomowej zwiększania 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 korzysta z operacji transakcji, więc nie jest automatycznie ponownie uruchamiany, jeśli wystąpiła aktualizacja powodująca konflikt. Operacja zwiększania wartości występuje jednak bezpośrednio na serwerze bazy danych, więc nie ma możliwości wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty związane z konkretną aplikacją, np. gdy użytkownik wyróżnia wpis, który wyróżnił już wcześniej, na potrzeby takiego przypadku użycia napisz niestandardowe reguły bezpieczeństwa.

Praca z danymi offline

Jeśli klient utraci połączenie z siecią, Twoja 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 są używane odsłuchy lub które są oznaczone jako dane, które mają być synchronizowane z serwerem. Podczas odczytu lub zapisu danych używana jest najpierw lokalna wersja danych. Następnie klient Firebase synchronizuje te dane ze zdalnymi serwerami baz danych i z innymi klientami według zasady „dokładamy wszelkich starań”.

W rezultacie wszystkie zapisy w bazie danych wywołują zdarzenia lokalne natychmiast, jeszcze przed jakąkolwiek interakcją 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 kodu niestandardowego.

Więcej informacji o działaniu offline znajdziesz w artykule Więcej informacji o możliwościach online i offline.

Dalsze kroki