Чтение и запись данных на 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

Чтобы читать данные из базы данных или записывать их в нее, вам нужен экземпляр 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(). Этот метод запускается один раз при подключении прослушивателя и снова каждый раз, когда данные, включая дочерние элементы, изменяются. Обратному вызову события передается снимок, содержащий все данные в этом местоположении, включая дочерние данные. Если данных нет, то при вызове exists() снимок вернет false, а при вызове getValue() – null.

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

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() вы можете обновить дочерние значения более низкого уровня, указав путь к ключу. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных, используя веерную рассылку данных. Например, в приложении для ведения блогов может быть класс 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 синхронизирует эти данные с удаленными серверами баз данных и другими клиентами по мере возможности.

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

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

Подробнее о работе в офлайн-режиме рассказывается в статье Возможности онлайн- и офлайн-режимов.

Дальнейшие действия