Odczytywanie i zapisywanie danych 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 odwołania. Odbiornik jest uruchamiany raz dla początkowego stanu danych i ponownie za każdym razem, gdy dane się zmieniają.

(Opcjonalnie) Prototypuj i testuj za pomocą Firebase Local Emulator Suite

Zanim omówimy sposób, w jaki Twoja aplikacja odczytuje i zapisuje w Bazie danych czasu rzeczywistego, przedstawmy zestaw narzędzi, których możesz użyć do prototypowania i testowania funkcjonalności Bazy danych czasu rzeczywistego: Firebase Local Emulator Suite. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły bezpieczeństwa lub pracujesz nad znalezieniem 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 czasu rzeczywistego jest częścią pakietu Local Emulator Suite, który umożliwia aplikacji interakcję z emulowaną zawartością i konfiguracją bazy danych, a także opcjonalnie 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, uruchamiając firebase emulators:start .
  3. Wykonywanie wywołań z kodu prototypu Twojej aplikacji przy użyciu pakietu SDK platformy Realtime Database w zwykły sposób lub przy użyciu interfejsu API REST bazy danych czasu rzeczywistego.

Dostępny jest szczegółowy przewodnik dotyczący bazy danych czasu rzeczywistego i funkcji chmury . Warto również zapoznać się z wprowadzeniem do pakietu Local Emulator Suite .

Pobierz DatabaseReference

Aby 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();

Zapisz dane

Podstawowe operacje zapisu

W przypadku podstawowych operacji zapisu można użyć setValue() w celu zapisania danych do określonego odwołania, zastępując wszelkie istniejące dane w tej ścieżce. Możesz użyć tej metody, aby:

  • Przekaż typy 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 właściwości, które mają zostać przypisane.

Jeśli używasz obiektu Java, zawartość twojego obiektu jest automatycznie mapowana do lokalizacji podrzędnych w sposób zagnieżdżony. Używanie 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 nadpisuje dane w określonej lokalizacji, w tym wszelkie węzły potomne. Jednak nadal możesz zaktualizować obiekt podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie ich 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);

Odczyt danych

Odczytywanie danych za pomocą trwał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 Oddzwonienie do zdarzenia Typowe użycie
ValueEventListener onDataChange() Odczytywanie i nasłuchiwanie zmian w całej zawartości ścieżki.

Możesz użyć metody onDataChange() do odczytania statycznej migawki zawartości w danej ścieżce, tak jak istniała ona w momencie zdarzenia. Ta metoda jest uruchamiana raz, gdy odbiornik jest podłączony, i ponownie za każdym razem, gdy zmieniają się dane, w tym elementy podrzędne. Wywołanie zwrotne zdarzenia przekazuje migawkę zawierającą wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma żadnych danych, migawka zwróci false , gdy wywołasz na niej 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 odbiera DataSnapshot , który zawiera dane w określonej lokalizacji w bazie danych w czasie zdarzenia. Wywołanie funkcji getValue() na migawce zwraca obiektową reprezentację danych w języku Java. Jeśli w danej lokalizacji nie istnieją żadne dane, wywołanie metody getValue() zwraca null .

W tym przykładzie ValueEventListener definiuje również metodę onCancelled() wywoływaną w przypadku anulowania odczytu. Na przykład odczyt można anulować, jeśli klient nie ma uprawnień do odczytu z lokalizacji bazy danych Firebase. Ta metoda przekazuje obiekt DatabaseError wskazujący, dlaczego wystąpił błąd.

Odczytaj dane raz

Przeczytaj raz za pomocą get()

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

Zasadniczo należy używać opisanych powyżej technik ValueEventListener do odczytywania danych w celu otrzymywania powiadomień o aktualizacjach danych z zaplecza. Techniki nasłuchiwania zmniejszają użycie i rozliczenia oraz są zoptymalizowane, aby zapewnić użytkownikom najlepsze wrażenia podczas korzystania z Internetu i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć funkcji get() , aby uzyskać migawkę danych z bazy danych. Jeśli z jakiegokolwiek powodu 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 doprowadzić do utraty wydajności, czemu można zapobiec, używając 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, używając słuchacza

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej została zwrócona natychmiast, zamiast sprawdzania zaktualizowanej wartości 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 muszą zostać załadowane tylko raz i nie oczekuje się, że będą się często zmieniać ani wymagać aktywnego słuchania. Na przykład aplikacja do blogowania w poprzednich przykładach używa tej metody do ładowania profilu użytkownika, gdy rozpoczyna on tworzenie nowego wpisu.

Aktualizowanie lub usuwanie danych

Zaktualizuj określone pola

Aby jednocześnie zapisywać do określonych węzłów potomnych bez nadpisywania innych węzłów potomnych, należy użyć metody updateChildren() .

Wywołując funkcję updateChildren() , możesz zaktualizować wartości potomne niższego poziomu, określając ścieżkę dla klucza. Jeśli dane są przechowywane w wielu lokalizacjach w celu lepszego skalowania, możesz zaktualizować wszystkie instancje tych danych, korzystając z funkcji rozkładania danych . Na przykład aplikacja do blogowania społecznościowego może mieć taką 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 publikującego, 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 push() do utworzenia posta w węźle zawierającym posty dla wszystkich użytkowników w /posts/$postid i jednoczesnego pobrania klucza za pomocą getKey() . Klucz może być następnie użyty do utworzenia drugiego wpisu w postach użytkownika w /user-posts/$userid/$postid .

Korzystając z tych ścieżek, możesz przeprowadzać jednoczesne aktualizacje w wielu lokalizacjach w drzewie JSON za pomocą jednego wywołania updateChildren() , tak jak w tym przykładzie tworzony jest nowy wpis w obu lokalizacjach. Jednoczesne aktualizacje przeprowadzane w ten sposób są niepodzielne: albo wszystkie aktualizacje powiodą się, albo wszystkie aktualizacje się nie powiodą.

Dodaj wywołanie zwrotne zakończenia

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, możesz dodać odbiornik uzupełniania. Zarówno setValue() jak i updateChildren() przyjmują opcjonalny detektor uzupełniania, który jest wywoływany, gdy zapis został pomyślnie zatwierdzony w bazie danych. Jeśli wywołanie nie powiodło się, słuchacz otrzymuje 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żesz także usunąć, podając null jako wartość dla innej operacji zapisu, takiej jak setValue() lub updateChildren() . Tej techniki można użyć z updateChildren() w celu usunięcia wielu elementów podrzędnych w jednym wywołaniu interfejsu API.

Odłącz słuchaczy

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

Jeśli detektor został dodany wiele razy do lokalizacji danych, jest wywoływany wiele razy dla każdego zdarzenia i należy go odłączyć taką samą liczbę razy, aby całkowicie go usunąć.

Wywołanie metody removeEventListener() na odbiorniku nadrzędnym nie powoduje automatycznego usunięcia detektorów zarejestrowanych w jego węzłach podrzędnych; removeEventListener() musi być również wywołana na wszystkich słuchaczach 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 jako argument bieżący stan danych i zwraca nowy żądany stan, który chcesz zapisać. Jeśli inny klient zapisze w lokalizacji przed pomyślnym zapisaniem nowej wartości, funkcja aktualizacji zostanie ponownie wywołana z nową bieżącą wartością i ponowiona zostanie próba zapisu.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i usuwaniem gwiazdek oraz śledzić, ile gwiazdek 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);
        }
    });
}

Korzystanie z transakcji zapobiega nieprawidłowemu liczeniu gwiazdek, jeśli wielu użytkowników oznacza ten sam post w tym samym czasie lub klient ma nieaktualne dane. Jeśli 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 przykładzie użycia zapisujemy w bazie danych dwie wartości: identyfikator użytkownika, który oznaczył/usunął post gwiazdką, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznacza post gwiazdką, zamiast transakcji możemy użyć atomowej operacji inkrementacji.

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 uruchamiany ponownie, jeśli występuje 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 wykryć i odrzucić konflikty specyficzne dla aplikacji, takie jak oznaczenie przez użytkownika posta, który już wcześniej oznaczył gwiazdką, należy 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, na których używane są odbiorniki lub które są oznaczone jako synchronizowane z serwerem. Gdy dane są odczytywane lub zapisywane, najpierw używana jest 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 do bazy danych wyzwalają zdarzenia lokalne natychmiast, przed jakąkolwiek interakcją z serwerem. Oznacza to, że Twoja aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub łączności.

Po przywróceniu łączności 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 na temat zachowania w trybie offline znajdziesz w artykule Dowiedz się więcej o możliwościach w trybie online i offline .

Następne kroki