Чтение и запись данных на платформах Apple

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

Получить FIRDatabaseReference

Для чтения или записи данных из базы данных вам необходим экземпляр FIRDatabaseReference :

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

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

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

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

Базовые операции записи

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

  • Типы передач, соответствующие доступным типам JSON, следующим образом:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Например, вы можете добавить пользователя с помощью setValue следующим образом:

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

Использование setValue таким образом перезаписывает данные в указанном месте, включая любые дочерние узлы. Однако вы все равно можете обновить дочерний элемент, не переписывая весь объект. Если вы хотите разрешить пользователям обновлять свои профили, вы можете обновить имя пользователя следующим образом:

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Прочитать данные

Чтение данных путем прослушивания событий значения

Чтобы прочитать данные по пути и прослушивать изменения, используйте observeEventType:withBlock из FIRDatabaseReference для наблюдения за событиями FIRDataEventTypeValue .

Тип события Типичное использование
FIRDataEventTypeValue Чтение и прослушивание изменений во всем содержимом пути.

Вы можете использовать событие FIRDataEventTypeValue для чтения данных по указанному пути, как они существуют во время события. Этот метод запускается один раз, когда прослушиватель присоединяется, и снова каждый раз, когда данные, включая дочерние, изменяются. Обратному вызову события передается snapshot содержащий все данные в этом месте, включая дочерние данные. Если данных нет, снимок вернет false при вызове exists() и nil при чтении его свойства value .

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

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

Слушатель получает FIRDataSnapshot , содержащий данные в указанном месте в базе данных на момент события в свойстве value . Вы можете назначить значения соответствующему собственному типу, например NSDictionary . Если в месте нет данных, value равно nil .

Прочитать данные один раз

Прочитайте один раз с помощью getData()

SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, находится ли ваше приложение в сети или офлайн.

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

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

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

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
NSString *userPath = [NSString stringWithFormat:@"users/%@/username", uid];
[[ref child:userPath] getDataWithCompletionBlock:^(NSError * _Nullable error, FIRDataSnapshot * _Nonnull snapshot) {
  if (error) {
    NSLog(@"Received an error %@", error);
    return;
  }
  NSString *userName = snapshot.value;
}];

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

Прочитайте данные один раз с наблюдателем

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

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

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
let userID = Auth.auth().currentUser?.uid
ref.child("users").child(userID!).observeSingleEvent(of: .value, with: { snapshot in
  // Get user value
  let value = snapshot.value as? NSDictionary
  let username = value?["username"] as? String ?? ""
  let user = User(username: username)

  // ...
}) { error in
  print(error.localizedDescription)
}

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
NSString *userID = [FIRAuth auth].currentUser.uid;
[[[_ref child:@"users"] child:userID] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  // Get user value
  User *user = [[User alloc] initWithUsername:snapshot.value[@"username"]];

  // ...
} withCancelBlock:^(NSError * _Nonnull error) {
  NSLog(@"%@", error.localizedDescription);
}];

Обновление или удаление данных

Обновить определенные поля

Чтобы одновременно записывать данные в определенные дочерние узлы узла, не перезаписывая другие дочерние узлы, используйте метод updateChildValues .

При вызове updateChildValues ​​вы можете обновить дочерние значения нижнего уровня, указав путь для ключа. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных с помощью data fan-out . Например, приложение для социальных блогов может захотеть создать публикацию и одновременно обновить ее в ленте последних действий и ленте действий пользователя, разместившего публикацию. Для этого приложение для блогов использует такой код:

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
guard let key = ref.child("posts").childByAutoId().key else { return }
let post = ["uid": userID,
            "author": username,
            "title": title,
            "body": body]
let childUpdates = ["/posts/\(key)": post,
                    "/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
NSString *key = [[_ref child:@"posts"] childByAutoId].key;
NSDictionary *post = @{@"uid": userID,
                       @"author": username,
                       @"title": title,
                       @"body": body};
NSDictionary *childUpdates = @{[@"/posts/" stringByAppendingString:key]: post,
                               [NSString stringWithFormat:@"/user-posts/%@/%@/", userID, key]: post};
[_ref updateChildValues:childUpdates];

В этом примере childByAutoId используется для создания поста в узле, содержащем посты для всех пользователей в /posts/$postid и одновременного получения ключа с помощью getKey() . Затем ключ можно использовать для создания второй записи в постах пользователя в /user-posts/$userid/$postid .

Используя эти пути, вы можете выполнять одновременные обновления в нескольких местах в дереве JSON с помощью одного вызова updateChildValues ​​, например, как этот пример создает новый пост в обоих местах. Одновременные обновления, выполненные таким образом, являются атомарными: либо все обновления завершаются успешно, либо все обновления завершаются неудачей.

Добавить блок завершения

Если вы хотите узнать, когда ваши данные были зафиксированы, вы можете добавить блок завершения. И setValue , и updateChildValues ​​принимают необязательный блок завершения, который вызывается, когда запись была зафиксирована в базе данных. Этот прослушиватель может быть полезен для отслеживания того, какие данные были сохранены, а какие все еще синхронизируются. Если вызов не удался, прослушивателю передается объект ошибки, указывающий причину сбоя.

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
do {
  try await ref.child("users").child(user.uid).setValue(["username": username])
  print("Data saved successfully!")
} catch {
  print("Data could not be saved: \(error).")
}

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
[[[_ref child:@"users"] child:user.uid] setValue:@{@"username": username} withCompletionBlock:^(NSError *error, FIRDatabaseReference *ref) {
  if (error) {
    NSLog(@"Data could not be saved: %@", error);
  } else {
    NSLog(@"Data saved successfully.");
  }
}];

Удалить данные

Самый простой способ удалить данные — вызвать removeValue для ссылки на местоположение этих данных.

Вы также можете удалить, указав nil в качестве значения для другой операции записи, такой как setValue или updateChildValues ​​. Вы можете использовать эту технику с updateChildValues ​​для удаления нескольких дочерних элементов за один вызов API.

Отсоединить слушателей

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

При добавлении блока обратного вызова к ссылке возвращается FIRDatabaseHandle . Эти дескрипторы можно использовать для удаления блока обратного вызова.

Если в ссылку на базу данных добавлено несколько слушателей, каждый слушатель вызывается при возникновении события. Чтобы остановить синхронизацию данных в этом месте, необходимо удалить всех наблюдателей в этом месте, вызвав метод removeAllObservers .

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

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

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

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

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
ref.runTransactionBlock({ (currentData: MutableData) -> TransactionResult in
  if var post = currentData.value as? [String: AnyObject],
    let uid = Auth.auth().currentUser?.uid {
    var stars: [String: Bool]
    stars = post["stars"] as? [String: Bool] ?? [:]
    var starCount = post["starCount"] as? Int ?? 0
    if let _ = stars[uid] {
      // Unstar the post and remove self from stars
      starCount -= 1
      stars.removeValue(forKey: uid)
    } else {
      // Star the post and add self to stars
      starCount += 1
      stars[uid] = true
    }
    post["starCount"] = starCount as AnyObject?
    post["stars"] = stars as AnyObject?

    // Set value and report transaction success
    currentData.value = post

    return TransactionResult.success(withValue: currentData)
  }
  return TransactionResult.success(withValue: currentData)
}) { error, committed, snapshot in
  if let error = error {
    print(error.localizedDescription)
  }
}

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
[ref runTransactionBlock:^FIRTransactionResult * _Nonnull(FIRMutableData * _Nonnull currentData) {
  NSMutableDictionary *post = currentData.value;
  if (!post || [post isEqual:[NSNull null]]) {
    return [FIRTransactionResult successWithValue:currentData];
  }

  NSMutableDictionary *stars = post[@"stars"];
  if (!stars) {
    stars = [[NSMutableDictionary alloc] initWithCapacity:1];
  }
  NSString *uid = [FIRAuth auth].currentUser.uid;
  int starCount = [post[@"starCount"] intValue];
  if (stars[uid]) {
    // Unstar the post and remove self from stars
    starCount--;
    [stars removeObjectForKey:uid];
  } else {
    // Star the post and add self to stars
    starCount++;
    stars[uid] = @YES;
  }
  post[@"stars"] = stars;
  post[@"starCount"] = @(starCount);

  // Set value and report transaction success
  currentData.value = post;
  return [FIRTransactionResult successWithValue:currentData];
} andCompletionBlock:^(NSError * _Nullable error,
                       BOOL committed,
                       FIRDataSnapshot * _Nullable snapshot) {
  // Transaction completed
  if (error) {
    NSLog(@"%@", error.localizedDescription);
  }
}];

Использование транзакции предотвращает некорректное подсчет звезд, если несколько пользователей одновременно отмечают одну и ту же запись или у клиента были устаревшие данные. Значение, содержащееся в классе FIRMutableData , изначально является последним известным клиенту значением для пути или nil , если его нет. Сервер сравнивает начальное значение с текущим значением и принимает транзакцию, если значения совпадают, или отклоняет ее. Если транзакция отклонена, сервер возвращает текущее значение клиенту, который снова запускает транзакцию с обновленным значением. Это повторяется до тех пор, пока транзакция не будет принята или пока не будет сделано слишком много попыток.

Атомарные приращения на стороне сервера

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

Быстрый

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
let updates = [
  "posts/\(postID)/stars/\(userID)": true,
  "posts/\(postID)/starCount": ServerValue.increment(1),
  "user-posts/\(postID)/stars/\(userID)": true,
  "user-posts/\(postID)/starCount": ServerValue.increment(1)
] as [String : Any]
Database.database().reference().updateChildValues(updates)

Objective-C

Примечание: этот продукт Firebase недоступен для целевой платформы App Clip.
NSDictionary *updates = @{[NSString stringWithFormat: @"posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"posts/%@/starCount", postID]: [FIRServerValue increment:@1],
                        [NSString stringWithFormat: @"user-posts/%@/stars/%@", postID, userID]: @TRUE,
                        [NSString stringWithFormat: @"user-posts/%@/starCount", postID]: [FIRServerValue increment:@1]};
[[[FIRDatabase database] reference] updateChildValues:updates];

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

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

Работа с данными офлайн

Если клиент потеряет сетевое соединение, ваше приложение продолжит работать корректно.

Каждый клиент, подключенный к базе данных Firebase, поддерживает собственную внутреннюю версию любых активных данных. Когда данные записываются, они сначала записываются в эту локальную версию. Затем клиент Firebase синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами по принципу «наилучших усилий».

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

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

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

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