Чтение и запись данных на 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 синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами, используя принцип «максимальных усилий».

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

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

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

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