Чтение и запись данных на платформах 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 .

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

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

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

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

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

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