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

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

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

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

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