(Необязательно) Создайте прототип и протестируйте его с помощью Firebase Emulator Suite.
Прежде чем говорить о том, как ваше приложение читает и записывает данные в Realtime Database, давайте познакомимся с набором инструментов, которые можно использовать для прототипирования и тестирования функциональности Realtime Database: Firebase Emulator Suite. Если вы экспериментируете с различными моделями данных, оптимизируете правила безопасности или ищете наиболее экономичный способ взаимодействия с бэкэндом, возможность работать локально без развертывания работающих сервисов может быть отличной идеей.
Эмулятор базы данных в реальном времени является частью пакета Emulator Suite, который позволяет вашему приложению взаимодействовать с содержимым и конфигурацией эмулируемой базы данных, а также, при необходимости, с ресурсами эмулируемого проекта (функциями, другими базами данных и правилами безопасности).
Использование эмулятора базы данных в реальном времени включает всего несколько шагов:
- Добавление строки кода в конфигурацию тестирования вашего приложения для подключения к эмулятору.
- В корневом каталоге вашего локального проекта выполните
firebase emulators:start. - Выполнение вызовов из прототипа кода вашего приложения с использованием SDK платформы Realtime Database как обычно, или с использованием REST API Realtime Database.
Подробное пошаговое руководство по работе с базами данных реального времени и облачными функциями доступно. Также рекомендуем ознакомиться с вводной информацией о пакете эмуляторов .
Получить ссылку на базу данных
Для чтения или записи данных из базы данных необходим экземпляр класса DatabaseReference :
DatabaseReference ref = FirebaseDatabase.instance.ref();
Запись данных
В этом документе рассматриваются основы чтения и записи данных в Firebase.
Данные Firebase записываются в DatabaseReference и извлекаются путем ожидания или прослушивания событий, генерируемых этим объектом. События генерируются один раз для начального состояния данных и повторно всякий раз, когда данные изменяются.
Основные операции записи
Для выполнения базовых операций записи можно использовать set() , чтобы сохранить данные по указанной ссылке, заменив существующие данные по этому пути. Можно установить ссылку на следующие типы: String , boolean , int , double , Map , List .
Например, добавить пользователя с помощью set() можно следующим образом:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
await ref.set({
"name": "John",
"age": 18,
"address": {
"line1": "100 Mountain View"
}
});
Использование set() таким образом перезаписывает данные в указанном месте, включая любые дочерние узлы. Однако вы все еще можете обновить дочерний узел, не перезаписывая весь объект. Если вы хотите разрешить пользователям обновлять свои профили, вы можете изменить имя пользователя следующим образом:
DatabaseReference ref = FirebaseDatabase.instance.ref("users/123");
// Only update the age, leave the name and address!
await ref.update({
"age": 19,
});
Метод ` update() принимает в качестве аргумента подпуть к узлам, что позволяет одновременно обновлять несколько узлов в базе данных:
DatabaseReference ref = FirebaseDatabase.instance.ref("users");
await ref.update({
"123/age": 19,
"123/address/line1": "1 Mountain View",
});
Прочитать данные
Считывайте данные, отслеживая события, отражающие их значения.
Для чтения данных по указанному пути и отслеживания изменений используйте свойство onValue объекта DatabaseReference для прослушивания событий DatabaseEvent .
Вы можете использовать DatabaseEvent для чтения данных по указанному пути в том виде, в котором они существуют на момент события. Это событие срабатывает один раз при подключении слушателя и снова каждый раз, когда изменяются данные, включая данные дочерних элементов. Событие имеет свойство snapshot , содержащее все данные в этом месте, включая данные дочерних элементов. Если данных нет, свойство exists объекта snapshot будет иметь false , а свойство value null.
Следующий пример демонстрирует приложение для ведения социальных блогов, извлекающее подробную информацию о публикации из базы данных:
DatabaseReference starCountRef =
FirebaseDatabase.instance.ref('posts/$postId/starCount');
starCountRef.onValue.listen((DatabaseEvent event) {
final data = event.snapshot.value;
updateStarCount(data);
});
Слушатель получает DataSnapshot , содержащий данные из указанного места в базе данных на момент события, в свойстве value .
Прочитайте данные один раз
Прочитайте один раз, используя метод get().
SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, работает ли ваше приложение в онлайн- или офлайн-режиме.
Как правило, для чтения данных и получения уведомлений об обновлениях данных из бэкэнда следует использовать описанные выше методы обработки событий значений. Эти методы сокращают потребление ресурсов и расходы, а также оптимизированы для обеспечения наилучшего пользовательского опыта как в онлайн, так и в офлайн-режиме.
Если данные нужны только один раз, вы можете использовать get() для получения снимка данных из базы данных. Если по какой-либо причине get() не может вернуть значение с сервера, клиент проверит локальный кэш и вернет ошибку, если значение по-прежнему не будет найдено.
Следующий пример демонстрирует однократное извлечение из базы данных общедоступного имени пользователя:
final ref = FirebaseDatabase.instance.ref();
final snapshot = await ref.child('users/$userId').get();
if (snapshot.exists) {
print(snapshot.value);
} else {
print('No data available.');
}
Излишнее использование метода get() может увеличить потребление полосы пропускания и привести к снижению производительности, чего можно избежать, используя слушатель реального времени, как показано выше.
Считывайте данные один раз с помощью функции once().
В некоторых случаях может потребоваться немедленное получение значения из локального кэша, вместо проверки обновления значения на сервере. В таких случаях можно использовать функцию once() для немедленного получения данных из локального дискового кэша.
Это полезно для данных, которые нужно загрузить только один раз и которые, как ожидается, не будут часто меняться или требовать активного отслеживания. Например, приложение для ведения блога в предыдущих примерах использует этот метод для загрузки профиля пользователя, когда он начинает писать новый пост:
final event = await ref.once(DatabaseEventType.value);
final username = event.snapshot.value?.username ?? 'Anonymous';
Обновление или удаление данных
Обновить определенные поля
Для одновременной записи данных в определенные дочерние узлы без перезаписи данных в других дочерних узлах используйте метод update() .
При вызове update() вы можете обновить значения дочерних элементов нижнего уровня, указав путь к ключу. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных, используя механизм расширения данных (data fan-out ). Например, приложение для ведения блога может захотеть создать публикацию и одновременно обновить ее в ленте последних действий и в ленте действий пользователя, опубликовавшего запись. Для этого приложение для ведения блога использует следующий код:
void writeNewPost(String uid, String username, String picture, String title,
String body) async {
// A post entry.
final postData = {
'author': username,
'uid': uid,
'body': body,
'title': title,
'starCount': 0,
'authorPic': picture,
};
// Get a key for a new Post.
final newPostKey =
FirebaseDatabase.instance.ref().child('posts').push().key;
// Write the new post's data simultaneously in the posts list and the
// user's post list.
final Map<String, Map> updates = {};
updates['/posts/$newPostKey'] = postData;
updates['/user-posts/$uid/$newPostKey'] = postData;
return FirebaseDatabase.instance.ref().update(updates);
}
В этом примере используется push() для создания записи в узле, содержащем записи всех пользователей, по адресу /posts/$postid , и одновременного получения ключа с помощью key . Затем этот ключ можно использовать для создания второй записи в записях пользователя по адресу /user-posts/$userid/$postid .
Используя эти пути, вы можете одновременно обновлять несколько мест в дереве JSON одним вызовом функции update() , как, например, в этом примере создается новый пост в обоих местах. Одновременные обновления, выполненные таким образом, являются атомарными: либо все обновления завершаются успешно, либо все обновления завершаются неудачей.
Добавить функцию обратного вызова для завершения.
Если вы хотите узнать, когда ваши данные были зафиксированы, вы можете зарегистрировать коллбэки завершения. Методы set() и update() возвращают объекты Future , к которым вы можете прикрепить коллбэки успеха и ошибки, вызываемые при фиксации записи в базу данных и при неудачном вызове.
FirebaseDatabase.instance
.ref('users/$userId/email')
.set(emailAddress)
.then((_) {
// Data saved successfully!
})
.catchError((error) {
// The write failed...
});
Удалить данные
Простейший способ удалить данные — вызвать метод remove() для ссылки на местоположение этих данных.
Удаление также можно выполнить, указав значение null в качестве значения для другой операции записи, например, set() или update() . Этот метод можно использовать с update() для удаления нескольких дочерних элементов за один вызов API.
Сохраняйте данные в виде транзакций.
При работе с данными, которые могут быть повреждены одновременными изменениями, например, с инкрементальными счетчиками, можно использовать транзакцию, передав обработчик транзакций в функцию runTransaction() . Обработчик транзакций принимает текущее состояние данных в качестве аргумента и возвращает новое желаемое состояние, которое вы хотите записать. Если другой клиент записывает данные в указанное место до того, как ваше новое значение будет успешно записано, ваша функция обновления вызывается снова с новым текущим значением, и попытка записи повторяется.
Например, в представленном приложении для ведения блога в социальных сетях вы можете разрешить пользователям отмечать и снимать отметки с постов, а также отслеживать количество полученных отметок следующим образом:
void toggleStar(String uid) async {
DatabaseReference postRef =
FirebaseDatabase.instance.ref("posts/foo-bar-123");
TransactionResult result = await postRef.runTransaction((Object? post) {
// Ensure a post at the ref exists.
if (post == null) {
return Transaction.abort();
}
Map<String, dynamic> _post = Map<String, dynamic>.from(post as Map);
if (_post["stars"] is Map && _post["stars"][uid] != null) {
_post["starCount"] = (_post["starCount"] ?? 1) - 1;
_post["stars"][uid] = null;
} else {
_post["starCount"] = (_post["starCount"] ?? 0) + 1;
if (!_post.containsKey("stars")) {
_post["stars"] = {};
}
_post["stars"][uid] = true;
}
// Return the new data.
return Transaction.success(_post);
});
}
По умолчанию события генерируются каждый раз при выполнении функции обновления транзакции, поэтому при многократном запуске функции могут возникать промежуточные состояния. Вы можете установить applyLocally в значение false , чтобы подавить эти промежуточные состояния и вместо этого дождаться завершения транзакции, прежде чем будут генерироваться события:
await ref.runTransaction((Object? post) {
// ...
}, applyLocally: false);
Результатом транзакции является объект TransactionResult , содержащий такую информацию, как подтверждение транзакции и новый снимок состояния:
DatabaseReference ref = FirebaseDatabase.instance.ref("posts/123");
TransactionResult result = await ref.runTransaction((Object? post) {
// ...
});
print('Committed? ${result.committed}'); // true / false
print('Snapshot? ${result.snapshot}'); // DataSnapshot
Отмена транзакции
Если вы хотите безопасно отменить транзакцию, вызовите метод Transaction.abort() чтобы сгенерировать исключение AbortTransactionException :
TransactionResult result = await ref.runTransaction((Object? user) {
if (user !== null) {
return Transaction.abort();
}
// ...
});
print(result.committed); // false
Атомарные приращения на стороне сервера
В описанном выше примере мы записываем в базу данных два значения: идентификатор пользователя, который ставит/снимает отметку с публикации, и увеличенное количество отметок. Если нам уже известно, что пользователь ставит отметку, мы можем использовать атомарную операцию увеличения вместо транзакции.
void addStar(uid, key) async {
Map<String, Object?> updates = {};
updates["posts/$key/stars/$uid"] = true;
updates["posts/$key/starCount"] = ServerValue.increment(1);
updates["user-posts/$key/stars/$uid"] = true;
updates["user-posts/$key/starCount"] = ServerValue.increment(1);
return FirebaseDatabase.instance.ref().update(updates);
}
Этот код не использует транзакционные операции, поэтому он не запускается автоматически при возникновении конфликтующих обновлений. Однако, поскольку операция инкремента выполняется непосредственно на сервере базы данных, вероятность конфликта исключена.
Если вы хотите обнаруживать и отклонять конфликты, специфичные для конкретного приложения, например, когда пользователь отмечает в избранное сообщение, которое он уже отмечал ранее, вам следует написать собственные правила безопасности для этого сценария использования.
Работа с данными в автономном режиме
Если клиент потеряет сетевое соединение, ваше приложение продолжит корректно работать.
Каждый клиент, подключенный к базе данных Firebase, поддерживает свою собственную внутреннюю версию активных данных. При записи данных сначала записывается именно в эту локальную версию. Затем клиент Firebase синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами, используя принцип «максимальных усилий».
В результате все операции записи в базу данных немедленно запускают локальные события, еще до того, как данные будут записаны на сервер. Это означает, что ваше приложение остается отзывчивым независимо от задержки сети или качества соединения.
После восстановления соединения ваше приложение получает соответствующий набор событий, благодаря чему клиент синхронизируется с текущим состоянием сервера, без необходимости написания какого-либо пользовательского кода.
Подробнее о поведении в офлайн-режиме мы поговорим в разделе «Узнайте больше об онлайн- и офлайн-возможностях» .