Odczytywanie i zapisywanie danych na Androidzie

W tym dokumencie omówiono podstawy odczytywania i zapisywania danych Firebase.

Dane Firebase są zapisywane w odwołaniu FirebaseDatabase i pobierane przez dołączenie asynchronicznego detektora do odwołania. Odbiornik jest wyzwalany raz dla początkowego stanu danych i ponownie za każdym razem, gdy dane się zmieniają.

(Opcjonalnie) Prototypowanie i testowanie za pomocą pakietu 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: Pakiet emulatorów lokalnych Firebase. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły zabezpieczeń 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ą lokalnego pakietu emulatorów, który umożliwia aplikacji interakcję z zawartością i konfiguracją emulowanej bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcje, inne bazy danych i reguły zabezpieczeń).

Korzystanie z emulatora bazy danych czasu rzeczywistego obejmuje tylko kilka kroków:

  1. Dodanie wiersza kodu do konfiguracji testowej aplikacji, aby połączyć się 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 zestawu SDK platformy Realtime Database jak zwykle lub przy użyciu interfejsu API REST Realtime Database.

Dostępny jest szczegółowy przewodnik dotyczący Bazy danych czasu rzeczywistego i funkcji w chmurze . Powinieneś także rzucić okiem na wprowadzenie Local Emulator Suite .

Uzyskaj referencję bazy danych

Aby odczytać lub zapisać dane z bazy danych, potrzebujesz instancji DatabaseReference :

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Zapisz dane

Podstawowe operacje zapisu

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

  • 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 żadnych argumentów i ma publiczne programy pobierające dla właściwości, które mają zostać przypisane.

Jeśli używasz obiektu Java, zawartość obiektu jest automatycznie mapowana na lokalizacje podrzędne 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, Twój obiekt User może wyglądać następująco:

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

}

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.
}

Możesz dodać użytkownika za pomocą setValue() w następujący sposób:

Java

public void writeNewUser(String userId, String name, String email) {
    User user = new User(name, email);

    mDatabase.child("users").child(userId).setValue(user);
}

Kotlin+KTX

fun writeNewUser(userId: String, name: String, email: String) {
    val user = User(name, email)

    database.child("users").child(userId).setValue(user)
}

Użycie setValue() w ten sposób powoduje nadpisanie danych w określonej lokalizacji, w tym wszelkich węzłach podrzędnych. Jednak nadal możesz zaktualizować dziecko 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:

Java

mDatabase.child("users").child(userId).child("username").setValue(name);

Kotlin+KTX

database.child("users").child(userId).child("username").setValue(name)

Przeczytaj dane

Odczytuj dane z wytrwałymi słuchaczami

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

Słuchacz Oddzwanianie do zdarzenia Typowe zastosowanie
ValueEventListener onDataChange() Czytaj i słuchaj zmian w całej zawartości ścieżki.

Możesz użyć metody onDataChange() , aby odczytać statyczną migawkę zawartości w danej ścieżce, tak jak istniała w momencie zdarzenia. Ta metoda jest wywoływana raz, gdy odbiornik jest podłączony, i ponownie za każdym razem, gdy dane, w tym dzieci, ulegną zmianie. Do wywołania zwrotnego zdarzenia przekazuje się migawkę zawierającą wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, migawka zwróci false , gdy wywołasz exists() i null , gdy wywołasz na nim getValue() .

Poniższy przykład ilustruje aplikację do blogowania społecznościowego pobierającą szczegóły wpisu z bazy danych:

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

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)

Odbiornik odbiera DataSnapshot , który zawiera dane w określonej lokalizacji w bazie danych w czasie zdarzenia. Wywołanie getValue() na migawce zwraca reprezentację danych obiektu Java. Jeśli w lokalizacji nie ma żadnych danych, wywołanie metody getValue() zwraca null .

W tym przykładzie ValueEventListener definiuje również 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. Ta metoda jest przekazywana do obiektu DatabaseError wskazującego przyczynę niepowodzenia.

Przeczytaj dane raz

Przeczytaj raz za pomocą get()

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

Ogólnie rzecz biorąc, należy użyć opisanych powyżej technik ValueEventListener , aby odczytać dane, aby otrzymywać powiadomienia o aktualizacjach danych z zaplecza. Techniki nasłuchiwania zmniejszają zużycie i opłaty oraz są zoptymalizowane, aby zapewnić użytkownikom jak najlepsze wrażenia w trybie online i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć get() , aby uzyskać migawkę danych z bazy danych. Jeśli z jakiegoś 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 get() może zwiększyć wykorzystanie przepustowości i doprowadzić do utraty wydajności, której można zapobiec, korzystając z nasłuchiwania w czasie rzeczywistym, jak pokazano powyżej.

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

Kotlin+KTX

mDatabase.child("users").child(userId).get().addOnSuccessListener {
    Log.i("firebase", "Got value ${it.value}")
}.addOnFailureListener{
    Log.e("firebase", "Error getting data", it)
}

Przeczytaj raz za pomocą 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żna użyć addListenerForSingleValueEvent , aby natychmiast pobrać dane z pamięci podręcznej dysku lokalnego.

Jest to przydatne w przypadku danych, które trzeba załadować tylko raz i nie oczekuje się, że będą się często zmieniać lub wymagają aktywnego nasłuchiwania. Na przykład aplikacja do blogowania w poprzednich przykładach używa tej metody do ładowania profilu użytkownika, gdy rozpoczyna on tworzenie nowego posta.

Aktualizacja lub usuwanie danych

Zaktualizuj określone pola

Aby jednocześnie pisać do określonych elementów podrzędnych węzła bez nadpisywania innych węzłów podrzędnych, użyj metody updateChildren() .

Wywołując updateChildren() , możesz zaktualizować wartości podrzędne niższego poziomu, określając ścieżkę klucza. Jeśli dane są przechowywane w wielu lokalizacjach w celu lepszego skalowania, możesz zaktualizować wszystkie wystąpienia tych danych za pomocą funkcji rozdzielania danych . Na przykład aplikacja do blogowania społecznościowego może mieć taką klasę Post :

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

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

Aby utworzyć post i jednocześnie zaktualizować go do ostatniego kanału aktywności i kanału aktywności użytkownika publikującego, aplikacja do blogowania używa takiego kodu:

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

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

Ten przykład używa funkcji push() do utworzenia wpisu w węźle zawierającym wpisy 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 we wpisach użytkownika w /user-posts/$userid/$postid .

Korzystając z tych ścieżek, możesz wykonywać jednoczesne aktualizacje w wielu lokalizacjach w drzewie JSON za pomocą jednego wywołania updateChildren() , na przykład jak ten przykład tworzy nowy wpis w obu lokalizacjach. Aktualizacje symultaniczne wykonane w ten sposób są niepodzielne: albo wszystkie aktualizacje się powiodą, albo wszystkie aktualizacje kończą się niepowodzeniem.

Dodaj zakończenie połączenia zwrotnego

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, możesz dodać detektor zakończenia. Zarówno setValue() , jak i updateChildren() pobierają opcjonalny detektor zakończenia, który jest wywoływany po pomyślnym zatwierdzeniu zapisu do bazy danych. Jeśli wywołanie nie powiodło się, obiektowi nasłuchującemu jest przekazywany obiekt błędu wskazujący przyczynę niepowodzenia.

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

Kotlin+KTX

database.child("users").child(userId).setValue(user)
        .addOnSuccessListener {
            // Write was successful!
            // ...
        }
        .addOnFailureListener {
            // Write failed
            // ...
        }

Usunąć dane

Najprostszym sposobem usunięcia danych jest wywołanie removeValue() w odniesieniu do lokalizacji tych danych.

Możesz również usunąć, określając null jako wartość dla innej operacji zapisu, takiej jak setValue() lub updateChildren() . Możesz użyć tej techniki z updateChildren() , aby usunąć wiele elementów podrzędnych w jednym wywołaniu 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 wielokrotnie dla każdego zdarzenia i aby usunąć go całkowicie, należy go odłączyć taką samą liczbę razy.

Wywołanie removeEventListener() na odbiorniku nadrzędnym nie usuwa automatycznie odbiornikó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 współbieżne 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 pożądany stan, który chcesz zapisać. Jeśli inny klient zapisuje w lokalizacji przed pomyślnym zapisaniem nowej wartości, funkcja aktualizacji jest wywoływana 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 zezwolić użytkownikom na oznaczanie i odznaczanie postów oraz śledzenie liczby gwiazdek, które otrzymał post w następujący sposób:

Java

private void onStarClicked(DatabaseReference postRef) {
    postRef.runTransaction(new Transaction.Handler() {
        @Override
        public Transaction.Result doTransaction(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);
        }
    });
}

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

Korzystanie z transakcji zapobiega błędnym zliczaniu gwiazdek, jeśli wielu użytkowników oznacza ten sam post w tym samym czasie lub klient ma nieaktualne dane. W przypadku odrzucenia transakcji serwer zwraca bieżącą wartość klientowi, który ponownie uruchamia transakcję z wartością zaktualizowaną. Powtarza się to do momentu zaakceptowania transakcji lub wykonania 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 oznacza post gwiazdką/usuwa gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli już wiemy, że użytkownik jest gwiazdą posta, zamiast transakcji możemy użyć atomowej operacji inkrementacji.

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

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

Ten kod nie używa operacji transakcji, więc nie jest automatycznie uruchamiany ponownie w przypadku konfliktu aktualizacji. Jednak ponieważ operacja inkrementacji odbywa się bezpośrednio na serwerze bazy danych, nie ma szans na konflikt.

Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, takie jak użytkownik oznaczony gwiazdką posta, który już wcześniej oznaczył, 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 połączony z bazą danych Firebase przechowuje własną wewnętrzną wersję wszystkich danych, na których są używane detektory lub które są oznaczone do utrzymywania synchronizacji z serwerem. Gdy dane są odczytywane lub zapisywane, ta lokalna wersja danych jest używana jako pierwsza. Klient Firebase synchronizuje następnie te dane ze zdalnymi serwerami baz danych oraz z innymi klientami na zasadzie „najlepszych starań”.

W rezultacie wszystkie zapisy w bazie danych wyzwalają zdarzenia lokalne natychmiast, przed jakąkolwiek interakcją z serwerem. Oznacza to, że Twoja aplikacja pozostaje responsywna bez względu na opóźnienie sieci lub łączność.

Po ponownym nawiązaniu łą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 o zachowaniu w trybie offline omówimy w artykule Dowiedz się więcej o możliwościach trybu online i offline .

Następne kroki