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

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

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+KTX

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+KTX

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

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+KTX

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

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

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+KTX

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

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

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

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

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

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

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