Чтение и запись данных в Интернете

(Необязательно) Создайте прототип и протестируйте его с помощью 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 .

Получите ссылку на базу данных.

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

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

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

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

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