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

(Необязательно) Создание прототипа и тестирование с помощью Firebase Local Emulator Suite

Прежде чем говорить о том, как ваше приложение читает и записывает в базу данных реального времени, давайте представим набор инструментов, которые вы можете использовать для прототипирования и тестирования функциональности базы данных реального времени: Firebase Local Emulator Suite. Если вы пробуете различные модели данных, оптимизируете свои правила безопасности или ищете наиболее экономичный способ взаимодействия с серверной частью, возможность работать локально без развертывания действующих сервисов может оказаться отличной идеей.

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

Использование эмулятора базы данных реального времени включает в себя всего несколько шагов:

  1. Добавление строки кода в тестовую конфигурацию вашего приложения для подключения к эмулятору.
  2. Из корня вашего локального каталога проекта запустите firebase emulators:start .
  3. Выполнение вызовов из кода прототипа вашего приложения с помощью SDK платформы базы данных реального времени, как обычно, или с помощью REST API базы данных реального времени.

Доступно подробное пошаговое руководство с использованием базы данных реального времени и облачных функций . Вам также следует ознакомиться с введением в Local Emulator Suite .

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

Чтобы читать или записывать данные из базы данных, вам нужен экземпляр firebase.database.Reference :

Web modular API

import { getDatabase } from "firebase/database";

const database = getDatabase();

Web namespaced API

var database = firebase.database();

Запись данных

В этом документе рассматриваются основы извлечения данных, а также порядок и фильтрация данных Firebase.

Данные Firebase извлекаются путем присоединения асинхронного прослушивателя к firebase.database.Reference . Слушатель запускается один раз для начального состояния данных и снова при каждом изменении данных.

Основные операции записи

Для основных операций записи вы можете использовать set() для сохранения данных по указанной ссылке, заменяя любые существующие данные по этому пути. Например, приложение для ведения социального блога может добавить пользователя с помощью set() следующим образом:

Web modular API

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 namespaced API

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 modular API

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 namespaced API

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 modular API

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 namespaced API

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 modular API

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 namespaced API

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() вы можете обновить дочерние значения более низкого уровня, указав путь для ключа. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных с помощью разветвления данных .

Например, приложение для ведения социальных блогов может создать публикацию и одновременно обновить ее в ленте последних действий и в ленте активности пользователя, отправившего публикацию, используя такой код:

Web modular API

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 namespaced API

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 modular API

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 namespaced API

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, вы можете использовать Promise . И set() и update() могут возвращать Promise которое вы можете использовать, чтобы узнать, когда запись зафиксирована в базе данных.

Отключить слушателей

Обратные вызовы удаляются путем вызова метода off() в вашей ссылке на базу данных Firebase.

Вы можете удалить одного слушателя, передав его в качестве параметра off() . Вызов off() для местоположения без аргументов удаляет всех слушателей в этом местоположении.

Вызов off() для родительского прослушивателя не приводит к автоматическому удалению прослушивателей, зарегистрированных на его дочерних узлах; off() также должен быть вызван для любых дочерних прослушивателей, чтобы удалить обратный вызов.

Сохранить данные как транзакции

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

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

Web modular API

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 namespaced API

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 modular API

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 namespaced API

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

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

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

Подробнее о поведении в автономном режиме мы поговорим в разделе Дополнительные сведения о возможностях в сети и в автономном режиме .

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