Odczytuj i zapisuj dane na Androidzie

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 aplikacja odczytuje i zapisuje w bazie danych Realtime Database, przedstawmy zestaw narzędzi, których można 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 w czasie rzeczywistym jest częścią pakietu Local Emulator Suite, który umożliwia Twojej aplikacji interakcję 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:

  1. Dodanie wiersza kodu do konfiguracji testowej aplikacji w celu nawiązania połączenia z emulatorem.
  2. Z katalogu głównego lokalnego katalogu projektu uruchom firebase emulators:start .
  3. 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 funkcję push() do utworzenia postu 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 aby całkowicie go usunąć, należy go odłączyć tyle samo razy.

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ą postu, 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 w bazie 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 .

Następne kroki