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

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

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

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

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

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

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

Получить FIRDatabaseReference

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

Быстрый

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

ref = Database.database().reference()

Цель-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])

Цель-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)

Цель-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
  // ...
})

Цель-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.
ref.child("users/\(uid)/username").getData(completion:  { error, snapshot in
  guard error == nil else {
    print(error!.localizedDescription)
    return;
  }
  let userName = snapshot.value as? String ?? "Unknown";
});

Цель-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)
}

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

Быстрый

Примечание. Этот продукт 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)

Цель-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.
ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Цель-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)
  }
}

Цель-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);

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

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

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

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

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