Чтение и запись данных на Android

В этом документе рассматриваются основы чтения и записи данных Firebase.

Данные Firebase записываются в ссылку FirebaseDatabase и извлекаются путём присоединения к ней асинхронного прослушивателя. Прослушиватель срабатывает один раз для начального состояния данных и затем каждый раз при их изменении.

(Необязательно) Создание прототипа и тестирование с помощью Firebase Local Emulator Suite

Прежде чем говорить о том, как ваше приложение считывает данные из Realtime Database и записывает их в неё, давайте рассмотрим набор инструментов, которые можно использовать для создания прототипа и тестирования функциональности Realtime Database : Firebase Local Emulator Suite . Если вы пробуете различные модели данных, оптимизируете правила безопасности или ищете наиболее экономичный способ взаимодействия с бэкендом, возможность работать локально, не разворачивая сервисы, может быть отличным решением.

Эмулятор Realtime Database является частью Local Emulator Suite , который позволяет вашему приложению взаимодействовать с содержимым и конфигурацией эмулируемой базы данных, а также, при необходимости, с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).

Использование эмулятора Realtime Database включает всего несколько шагов:

  1. Добавление строки кода в тестовую конфигурацию вашего приложения для подключения к эмулятору.
  2. Из корня локального каталога проекта запустите firebase emulators:start .
  3. Выполнение вызовов из прототипного кода вашего приложения с использованием SDK платформы Realtime Database обычным образом или с использованием REST API Realtime Database .

Доступно подробное пошаговое руководство по работе с Realtime Database и Cloud Functions . Также рекомендуем ознакомиться с введением в Local Emulator Suite .

Получить ссылку на базу данных

Для чтения или записи данных из базы данных вам необходим экземпляр DatabaseReference :

Kotlin

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

Java

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

Запись данных

Базовые операции записи

Для базовых операций записи можно использовать setValue() , чтобы сохранить данные по указанной ссылке, заменив все существующие данные по этому пути. Этот метод можно использовать для:

  • Типы передачи, соответствующие доступным типам JSON, следующие:
    • String
    • Long
    • Double
    • Boolean
    • Map<String, Object>
    • List<Object>
  • Передайте пользовательский объект Java, если класс, который его определяет, имеет конструктор по умолчанию, который не принимает аргументов и имеет открытые методы получения для назначаемых свойств.

При использовании объекта Java его содержимое автоматически сопоставляется с дочерними расположениями во вложенном порядке. Использование объекта Java также обычно делает код более читаемым и простым в поддержке. Например, если у вас есть приложение с базовым профилем пользователя, объект User может выглядеть следующим образом:

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

}

Вы можете добавить пользователя с помощью setValue() следующим образом:

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

Использование setValue() таким образом перезаписывает данные в указанном месте, включая все дочерние узлы. Однако вы всё равно можете обновить дочерний элемент, не переписывая весь объект. Если вы хотите разрешить пользователям обновлять свои профили, вы можете обновить имя пользователя следующим образом:

Kotlin

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

Java

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

Чтение данных

Чтение данных с помощью постоянных прослушивателей

Чтобы прочитать данные по пути и прослушать изменения, используйте метод addValueEventListener() для добавления ValueEventListener к DatabaseReference .

Слушатель Обратный вызов события Типичное использование
ValueEventListener onDataChange() Чтение и прослушивание изменений во всем содержимом пути.

Метод onDataChange() позволяет прочитать статический снимок содержимого по заданному пути в том виде, в котором оно существовало на момент события. Этот метод срабатывает один раз при подключении прослушивателя и затем каждый раз при изменении данных, включая дочерние элементы. В функцию обратного вызова события передаётся снимок, содержащий все данные в этом месте, включая дочерние элементы. Если данные отсутствуют, снимок вернёт значение false при вызове exists() и null при вызове getValue() .

В следующем примере показано приложение для социального блоггинга, извлекающее сведения о посте из базы данных:

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

Слушатель получает DataSnapshot , содержащий данные, находящиеся в указанном месте базы данных на момент события. Вызов getValue() для снимка возвращает представление данных в виде объекта Java. Если в указанном месте данные отсутствуют, вызов метода getValue() возвращает null .

В этом примере ValueEventListener также определяет метод onCancelled() , который вызывается при отмене чтения. Например, чтение может быть отменено, если у клиента нет разрешения на чтение из расположения базы данных Firebase. Этому методу передаётся объект DatabaseError , указывающий причину сбоя.

Прочитать данные один раз

Прочитать один раз с помощью get()

SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, находится ли ваше приложение в сети или офлайн.

Как правило, для чтения данных и получения уведомлений об их обновлении из бэкенда следует использовать описанные выше методы ValueEventListener . Методы прослушивания сокращают потребление ресурсов и расходы на оплату, а также оптимизированы для обеспечения максимального удобства пользователей как в сети, так и в автономном режиме.

Если данные нужны вам только один раз, вы можете использовать get() для получения снимка данных из базы данных. Если по какой-либо причине get() не может вернуть значение с сервера, клиент проверит кэш локального хранилища и вернет ошибку, если значение по-прежнему не найдено.

Ненужное использование get() может увеличить использование полосы пропускания и привести к потере производительности, чего можно избежать, используя прослушиватель в реальном времени, как показано выше.

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

Прочитать один раз с помощью прослушивателя

В некоторых случаях может потребоваться немедленное получение значения из локального кэша, без необходимости проверки наличия обновленного значения на сервере. В таких случаях можно использовать addListenerForSingleValueEvent для немедленного получения данных из локального дискового кэша.

Это полезно для данных, которые нужно загрузить только один раз и которые не будут часто меняться или требовать активного прослушивания. Например, приложение для ведения блогов в предыдущих примерах использует этот метод для загрузки профиля пользователя, когда тот начинает писать новую публикацию.

Обновление или удаление данных

Обновить определенные поля

Для одновременной записи в определенные дочерние узлы узла без перезаписи других дочерних узлов используйте метод updateChildren() .

При вызове метода updateChildren() можно обновить значения дочерних элементов нижнего уровня, указав путь к ключу. Если данные хранятся в нескольких местах для лучшего масштабирования, можно обновить все экземпляры этих данных с помощью функции data fan-out . Например, приложение для социальных блогов может иметь класс Post следующего вида:

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

Чтобы создать публикацию и одновременно обновить ее в ленте последних действий и ленте действий пользователя, разместившего публикацию, приложение для ведения блога использует следующий код:

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

В этом примере push() используется для создания записи в узле, содержащем записи всех пользователей в /posts/$postid и одновременного получения ключа с помощью getKey() . Затем этот ключ можно использовать для создания второй записи в записях пользователя в /user-posts/$userid/$postid .

Используя эти пути, вы можете выполнять одновременные обновления нескольких локаций в дереве JSON одним вызовом updateChildren() , например, как в этом примере создаётся новая запись в обеих локациях. Одновременные обновления, выполненные таким образом, являются атомарными: либо все обновления выполнены успешно, либо все обновления завершены неудачей.

Добавить обратный вызов завершения

Если вы хотите знать, когда данные были зафиксированы, вы можете добавить прослушиватель завершения. Методы setValue() и updateChildren() принимают необязательный прослушиватель завершения, который вызывается после успешного фиксирования записи в базу данных. Если вызов не удался, прослушивателю передаётся объект ошибки с указанием причины сбоя.

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

Удалить данные

Самый простой способ удалить данные — вызвать removeValue() для ссылки на местоположение этих данных.

Вы также можете удалить элемент, указав null в качестве значения для другой операции записи, например setValue() или updateChildren() . Этот метод можно использовать с updateChildren() для удаления нескольких дочерних элементов за один вызов API.

Отсоединить слушателей

Обратные вызовы удаляются путем вызова метода removeEventListener() в ссылке на базу данных Firebase.

Если прослушиватель был добавлен к местоположению данных несколько раз, он вызывается несколько раз для каждого события, и для его полного удаления необходимо отсоединить его столько же раз.

Вызов removeEventListener() для родительского прослушивателя не приводит к автоматическому удалению прослушивателей, зарегистрированных на его дочерних узлах; removeEventListener() также необходимо вызвать для любых дочерних прослушивателей, чтобы удалить обратный вызов.

Сохранить данные как транзакции

При работе с данными, которые могут быть повреждены одновременными изменениями, такими как инкрементные счётчики, можно использовать транзакционную операцию . Эта операция принимает два аргумента: функцию обновления и необязательный обратный вызов для завершения. Функция обновления принимает текущее состояние данных в качестве аргумента и возвращает новое желаемое состояние, которое вы хотите записать. Если другой клиент выполнит запись в это место до того, как ваше новое значение будет успешно записано, ваша функция обновления будет вызвана снова с новым текущим значением, и попытка записи будет повторена.

Например, в примере приложения для ведения социального блога вы можете разрешить пользователям отмечать и удалять звездочки с постов, а также отслеживать, сколько звезд получила публикация, следующим образом:

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

Использование транзакций предотвращает некорректное подсчёт звёзд, если несколько пользователей одновременно отметили одну и ту же публикацию или у клиента были устаревшие данные. Если транзакция отклонена, сервер возвращает клиенту текущее значение, и он снова запускает транзакцию с обновлённым значением. Это повторяется до тех пор, пока транзакция не будет принята или пока не будет сделано слишком много попыток.

Атомарные приращения на стороне сервера

В приведённом выше примере мы записываем в базу данных два значения: идентификатор пользователя, который добавляет/удаляет звёздочку с публикации, и приращение количества звёзд. Если мы уже знаем, что пользователь добавляет звёздочку к публикации, мы можем использовать атомарную операцию приращения вместо транзакции.

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

Этот код не использует транзакцию, поэтому он не перезапускается автоматически при возникновении конфликтующего обновления. Однако, поскольку операция увеличения выполняется непосредственно на сервере базы данных, вероятность возникновения конфликта исключена.

Если вы хотите обнаружить и отклонить конфликты, специфичные для приложения, например, когда пользователь отмечает публикацию, которую он уже отметил ранее, вам следует написать специальные правила безопасности для этого варианта использования.

Работа с данными офлайн

Если клиент потеряет сетевое соединение, ваше приложение продолжит корректно работать.

Каждый клиент, подключенный к базе данных Firebase, хранит собственную внутреннюю версию любых данных, к которым обращаются слушатели или которые помечены как требующие синхронизации с сервером. При чтении или записи данных эта локальная версия используется в первую очередь. Затем клиент Firebase синхронизирует эти данные с удаленными серверами баз данных и другими клиентами по принципу «максимальных усилий».

В результате все записи в базу данных запускают локальные события немедленно, до любого взаимодействия с сервером. Это означает, что ваше приложение остаётся отзывчивым независимо от задержек в сети или состояния подключения.

После восстановления соединения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости написания какого-либо пользовательского кода.

Подробнее о поведении вне сети мы поговорим в статье «Узнайте больше о возможностях онлайн и офлайн» .

Следующие шаги