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

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

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

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

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

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

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

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

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

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

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

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

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

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