(Необязательно) Создание прототипа и тестирование с помощью 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 синхронизирует эти данные с удаленными серверами баз данных и другими клиентами по принципу «максимальных усилий».
В результате все записи в базу данных запускают локальные события немедленно, ещё до того, как данные будут записаны на сервер. Это означает, что ваше приложение остаётся отзывчивым независимо от задержек в сети или проблем с подключением.
После восстановления соединения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости написания какого-либо пользовательского кода.
Подробнее о поведении вне сети мы поговорим в статье «Узнайте больше о возможностях онлайн и офлайн» .