Этот документ охватывает основы чтения и записи данных Firebase.
Данные Firebase записываются в ссылку FirebaseDatabase
и извлекаются путем присоединения к ссылке асинхронного прослушивателя. Слушатель запускается один раз для начального состояния данных и снова при каждом изменении данных.
(Необязательно) Создание прототипа и тестирование с помощью Firebase Local Emulator Suite
Прежде чем говорить о том, как ваше приложение читает и записывает в базу данных реального времени, давайте представим набор инструментов, которые вы можете использовать для прототипирования и тестирования функциональности базы данных реального времени: Firebase Local Emulator Suite. Если вы пробуете разные модели данных, оптимизируете правила безопасности или работаете над поиском наиболее экономичного способа взаимодействия с серверной частью, возможность работать локально без развертывания действующих сервисов может оказаться отличной идеей.
Эмулятор базы данных реального времени является частью набора локальных эмуляторов, который позволяет вашему приложению взаимодействовать с содержимым и конфигурацией эмулируемой базы данных, а также, при необходимости, с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).
Использование эмулятора базы данных реального времени включает в себя всего несколько шагов:
- Добавление строки кода в тестовую конфигурацию вашего приложения для подключения к эмулятору.
- Из корня вашего локального каталога проекта запустите
firebase emulators:start
. - Выполнение вызовов из кода прототипа вашего приложения с помощью SDK платформы базы данных реального времени, как обычно, или с помощью REST API базы данных реального времени.
Доступно подробное пошаговое руководство с использованием базы данных реального времени и облачных функций . Вам также следует ознакомиться с введением в 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 синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами на основе «максимальных усилий».
В результате все операции записи в базу данных инициируют локальные события немедленно, до любого взаимодействия с сервером. Это означает, что ваше приложение остается отзывчивым независимо от сетевой задержки или подключения.
После восстановления подключения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо пользовательский код.
Подробнее о поведении в автономном режиме мы поговорим в разделе Дополнительные сведения о возможностях в сети и в автономном режиме .