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

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

Вы можете использовать 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 s, к которым вы можете прикрепить обратные вызовы успеха и ошибки, которые вызываются, когда запись была зафиксирована в базе данных и когда вызов не удался.

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

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

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

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

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