(Необязательно) Создание прототипа и тестирование с помощью Firebase Local Emulator Suite
Прежде чем говорить о том, как ваше приложение считывает и записывает данные в Realtime Database , давайте рассмотрим набор инструментов, которые вы можете использовать для прототипирования и тестирования функциональности Realtime Database : Firebase Local Emulator Suite . Если вы пробуете разные модели данных, оптимизируете правила безопасности или пытаетесь найти наиболее экономичный способ взаимодействия с бэкэндом, возможность работать локально без развертывания живых сервисов может быть отличной идеей.
Эмулятор Realtime Database является частью Local Emulator Suite , который позволяет вашему приложению взаимодействовать с содержимым и конфигурацией эмулируемой базы данных, а также, при необходимости, с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).
Использование эмулятора Realtime Database включает в себя всего несколько шагов:
- Добавление строки кода в тестовую конфигурацию вашего приложения для подключения к эмулятору.
- Из корня локального каталога проекта запустите
firebase emulators:start
. - Выполнение вызовов из кода прототипа вашего приложения с использованием SDK платформы Realtime Database обычным способом или с использованием REST API Realtime Database .
Подробный обзор Realtime Database и Cloud Functions доступен. Вам также следует ознакомиться с введением в Local Emulator Suite .
Получить ссылку на базу данных
Для чтения или записи данных из базы данных вам необходим экземпляр firebase.database.Reference
:
Web
import { getDatabase } from "firebase/database"; const database = getDatabase();
Web
var database = firebase.database();
Запись данных
В этом документе рассматриваются основы извлечения данных, а также порядок упорядочивания и фильтрации данных Firebase.
Данные Firebase извлекаются путем присоединения асинхронного прослушивателя к firebase.database.Reference
. Прослушиватель срабатывает один раз для начального состояния данных и снова каждый раз при изменении данных.
Базовые операции записи
Для базовых операций записи вы можете использовать set()
для сохранения данных по указанной ссылке, заменяя любые существующие данные по этому пути. Например, приложение для социальных блогов может добавить пользователя с помощью set()
следующим образом:
Web
import { getDatabase, ref, set } from "firebase/database"; function writeUserData(userId, name, email, imageUrl) { const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }); }
Web
function writeUserData(userId, name, email, imageUrl) { firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }); }
Использование set()
перезаписывает данные в указанном месте, включая все дочерние узлы.
Прочитать данные
Прислушивайтесь к ценностным событиям
Чтобы прочитать данные по пути и прослушать изменения, используйте onValue()
для наблюдения за событиями. Вы можете использовать это событие для чтения статических снимков содержимого по указанному пути, как они существовали во время события. Этот метод запускается один раз, когда прослушиватель присоединяется, и снова каждый раз, когда данные, включая дочерние, изменяются. Обратному вызову события передается снимок, содержащий все данные по этому местоположению, включая дочерние данные. Если данных нет, снимок вернет false
при вызове exists()
и null
при вызове val()
для него.
В следующем примере показано, как приложение для ведения социального блога извлекает количество звезд поста из базы данных:
Web
import { getDatabase, ref, onValue } from "firebase/database"; const db = getDatabase(); const starCountRef = ref(db, 'posts/' + postId + '/starCount'); onValue(starCountRef, (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
Web
var starCountRef = firebase.database().ref('posts/' + postId + '/starCount'); starCountRef.on('value', (snapshot) => { const data = snapshot.val(); updateStarCount(postElement, data); });
Слушатель получает snapshot
, содержащий данные в указанном месте в базе данных на момент события. Вы можете получить данные в snapshot
с помощью метода val()
.
Прочитать данные один раз
Прочитайте данные один раз с помощью get()
SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, находится ли ваше приложение в сети или офлайн.
Как правило, вам следует использовать описанные выше методы событий значений для чтения данных, чтобы получать уведомления об обновлениях данных из бэкэнда. Методы прослушивания сокращают использование и выставление счетов и оптимизированы для предоставления вашим пользователям наилучшего опыта при выходе в онлайн и офлайн.
Если данные нужны вам только один раз, вы можете использовать get()
для получения снимка данных из базы данных. Если по какой-либо причине get()
не может вернуть значение сервера, клиент проверит локальный кэш хранилища и вернет ошибку, если значение все еще не найдено.
Ненужное использование get()
может увеличить использование полосы пропускания и привести к потере производительности, чего можно избежать, используя прослушиватель в реальном времени, как показано выше.
Web
import { getDatabase, ref, child, get } from "firebase/database"; const dbRef = ref(getDatabase()); get(child(dbRef, `users/${userId}`)).then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
Web
const dbRef = firebase.database().ref(); dbRef.child("users").child(userId).get().then((snapshot) => { if (snapshot.exists()) { console.log(snapshot.val()); } else { console.log("No data available"); } }).catch((error) => { console.error(error); });
Прочитайте данные один раз с наблюдателем
В некоторых случаях вам может понадобиться, чтобы значение из локального кэша было возвращено немедленно, вместо проверки обновленного значения на сервере. В этих случаях вы можете использовать once()
чтобы получить данные из локального дискового кэша немедленно.
Это полезно для данных, которые нужно загрузить только один раз и которые не должны часто меняться или требовать активного прослушивания. Например, приложение для ведения блогов в предыдущих примерах использует этот метод для загрузки профиля пользователя, когда он начинает писать новый пост:
Web
import { getDatabase, ref, onValue } from "firebase/database"; import { getAuth } from "firebase/auth"; const db = getDatabase(); const auth = getAuth(); const userId = auth.currentUser.uid; return onValue(ref(db, '/users/' + userId), (snapshot) => { const username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... }, { onlyOnce: true });
Web
var userId = firebase.auth().currentUser.uid; return firebase.database().ref('/users/' + userId).once('value').then((snapshot) => { var username = (snapshot.val() && snapshot.val().username) || 'Anonymous'; // ... });
Обновление или удаление данных
Обновить определенные поля
Чтобы одновременно записывать данные в определенные дочерние узлы узла, не перезаписывая другие дочерние узлы, используйте метод update()
.
При вызове update()
можно обновить дочерние значения нижнего уровня, указав путь для ключа. Если данные хранятся в нескольких местах для лучшего масштабирования, можно обновить все экземпляры этих данных с помощью data fan-out .
Например, приложение для социальных блогов может создать публикацию и одновременно обновить ее в ленте последних действий и ленте действий пользователя, разместившего публикацию, с помощью следующего кода:
Web
import { getDatabase, ref, child, push, update } from "firebase/database"; function writeNewPost(uid, username, picture, title, body) { const db = getDatabase(); // A post entry. const postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. const newPostKey = push(child(ref(db), 'posts')).key; // Write the new post's data simultaneously in the posts list and the user's post list. const updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return update(ref(db), updates); }
Web
function writeNewPost(uid, username, picture, title, body) { // A post entry. var postData = { author: username, uid: uid, body: body, title: title, starCount: 0, authorPic: picture }; // Get a key for a new Post. var newPostKey = firebase.database().ref().child('posts').push().key; // Write the new post's data simultaneously in the posts list and the user's post list. var updates = {}; updates['/posts/' + newPostKey] = postData; updates['/user-posts/' + uid + '/' + newPostKey] = postData; return firebase.database().ref().update(updates); }
В этом примере push()
используется для создания поста в узле, содержащем посты для всех пользователей в /posts/$postid
и одновременного извлечения ключа. Затем ключ может быть использован для создания второй записи в постах пользователя в /user-posts/$userid/$postid
.
Используя эти пути, вы можете выполнять одновременные обновления в нескольких местах в дереве JSON с помощью одного вызова update()
, например, как этот пример создает новый пост в обоих местах. Одновременные обновления, выполненные таким образом, являются атомарными: либо все обновления завершаются успешно, либо все обновления завершаются неудачей.
Добавить обратный вызов завершения
Если вы хотите узнать, когда ваши данные были зафиксированы, вы можете добавить обратный вызов завершения. И set()
, и update()
принимают необязательный обратный вызов завершения, который вызывается, когда запись была зафиксирована в базе данных. Если вызов не удался, обратному вызову передается объект ошибки, указывающий причину сбоя.
Web
import { getDatabase, ref, set } from "firebase/database"; const db = getDatabase(); set(ref(db, 'users/' + userId), { username: name, email: email, profile_picture : imageUrl }) .then(() => { // Data saved successfully! }) .catch((error) => { // The write failed... });
Web
firebase.database().ref('users/' + userId).set({ username: name, email: email, profile_picture : imageUrl }, (error) => { if (error) { // The write failed... } else { // Data saved successfully! } });
Удалить данные
Самый простой способ удалить данные — вызвать remove()
для ссылки на местоположение этих данных.
Вы также можете удалить, указав null
в качестве значения для другой операции записи, такой как set()
или update()
. Вы можете использовать эту технику с update()
для удаления нескольких дочерних элементов за один вызов API.
Получите Promise
Чтобы узнать, когда ваши данные будут переданы на сервер Firebase Realtime Database , вы можете использовать Promise
. И set()
, и update()
могут возвращать Promise
, который вы можете использовать, чтобы узнать, когда запись будет передана в базу данных.
Отсоединить слушателей
Обратные вызовы удаляются путем вызова метода off()
в ссылке на базу данных Firebase.
Вы можете удалить одного слушателя, передав его в качестве параметра off()
. Вызов off()
для местоположения без аргументов удаляет всех слушателей в этом местоположении.
Вызов off()
для родительского прослушивателя не приводит к автоматическому удалению прослушивателей, зарегистрированных на его дочерних узлах; off()
также необходимо вызвать для любых дочерних прослушивателей, чтобы удалить обратный вызов.
Сохраните данные как транзакции
При работе с данными, которые могут быть повреждены параллельными изменениями, такими как инкрементные счетчики, вы можете использовать транзакционную операцию . Вы можете предоставить этой операции функцию обновления и необязательный обратный вызов завершения. Функция обновления принимает текущее состояние данных в качестве аргумента и возвращает новое желаемое состояние, которое вы хотели бы записать. Если другой клиент записывает данные в местоположение до того, как ваше новое значение будет успешно записано, ваша функция обновления вызывается снова с новым текущим значением, и запись повторяется.
Например, в примере приложения для ведения социальных блогов вы можете разрешить пользователям отмечать и удалять звездочки с постов, а также отслеживать, сколько звезд получила публикация, следующим образом:
Web
import { getDatabase, ref, runTransaction } from "firebase/database"; function toggleStar(uid) { const db = getDatabase(); const postRef = ref(db, '/posts/foo-bar-123'); runTransaction(postRef, (post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
Web
function toggleStar(postRef, uid) { postRef.transaction((post) => { if (post) { if (post.stars && post.stars[uid]) { post.starCount--; post.stars[uid] = null; } else { post.starCount++; if (!post.stars) { post.stars = {}; } post.stars[uid] = true; } } return post; }); }
Использование транзакции предотвращает некорректное подсчет звезд, если несколько пользователей одновременно отмечают одну и ту же запись или у клиента устаревшие данные. Если транзакция отклонена, сервер возвращает клиенту текущее значение, который снова запускает транзакцию с обновленным значением. Это повторяется до тех пор, пока транзакция не будет принята или вы не отмените транзакцию.
Атомарные приращения на стороне сервера
В приведенном выше примере использования мы записываем два значения в базу данных: идентификатор пользователя, который отмечает/убирает звезду с поста, и увеличенное количество звезд. Если мы уже знаем, что пользователь отмечает пост, мы можем использовать атомарную операцию увеличения вместо транзакции.
Web
function addStar(uid, key) { import { getDatabase, increment, ref, update } from "firebase/database"; const dbRef = ref(getDatabase()); const updates = {}; updates[`posts/${key}/stars/${uid}`] = true; updates[`posts/${key}/starCount`] = increment(1); updates[`user-posts/${key}/stars/${uid}`] = true; updates[`user-posts/${key}/starCount`] = increment(1); update(dbRef, updates); }
Web
function addStar(uid, key) { const updates = {}; updates[`posts/${key}/stars/${uid}`] = true; updates[`posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); updates[`user-posts/${key}/stars/${uid}`] = true; updates[`user-posts/${key}/starCount`] = firebase.database.ServerValue.increment(1); firebase.database().ref().update(updates); }
Этот код не использует транзакционную операцию, поэтому он не будет автоматически перезапущен, если возникнет конфликтное обновление. Однако, поскольку операция приращения происходит непосредственно на сервере базы данных, вероятность конфликта исключена.
Если вы хотите обнаружить и отклонить конфликты, специфичные для приложения, например, когда пользователь отмечает публикацию, которую он уже отметил ранее, вам следует написать специальные правила безопасности для этого варианта использования.
Работа с данными офлайн
Если клиент потеряет сетевое соединение, ваше приложение продолжит работать корректно.
Каждый клиент, подключенный к базе данных Firebase, поддерживает собственную внутреннюю версию любых активных данных. Когда данные записываются, они сначала записываются в эту локальную версию. Затем клиент Firebase синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами по принципу «наилучших усилий».
В результате все записи в базу данных немедленно запускают локальные события, до того, как какие-либо данные будут записаны на сервер. Это означает, что ваше приложение остается отзывчивым независимо от сетевой задержки или подключения.
После восстановления соединения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера, без необходимости написания какого-либо специального кода.
Подробнее о поведении в автономном режиме мы поговорим в статье «Узнайте больше о возможностях в режиме онлайн и офлайн» .