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

(Optional) Prototype and test with Firebase Local Emulator Suite

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

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

Using the Realtime Database emulator involves just a few steps:

  1. Adding a line of code to your app's test config to connect to the emulator.
  2. From the root of your local project directory, running firebase emulators:start .
  3. Making calls from your app's prototype code using a Realtime Database platform SDK as usual, or using the Realtime Database REST API.

A detailed walkthrough involving Realtime Database and Cloud Functions is available. You should also have a look at the Local Emulator Suite introduction .

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

To read or write data from the database, you need an instance of FIRDatabaseReference :

Быстрый

Note: This Firebase product is not available on the App Clip target.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Note: This Firebase product is not available on the App Clip target.
@property (strong, nonatomic) FIRDatabaseReference *ref;

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

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

This document covers the basics of reading and writing Firebase data.

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

Основные операции записи

For basic write operations, you can use setValue to save data to a specified reference, replacing any existing data at that path. You can use this method to:

  • Pass types that correspond to the available JSON types as follows:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

For instance, you can add a user with setValue as follows:

Быстрый

Note: This Firebase product is not available on the App Clip target.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Note: This Firebase product is not available on the App Clip target.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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

Быстрый

Note: This Firebase product is not available on the App Clip target.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Note: This Firebase product is not available on the App Clip target.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

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

Считывайте данные, отслеживая события, отражающие их значения.

To read data at a path and listen for changes, use the observeEventType:withBlock of FIRDatabaseReference to observe FIRDataEventTypeValue events.

Тип события Типичное использование
FIRDataEventTypeValue Read and listen for changes to the entire contents of a path.

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

The following example demonstrates a social blogging application retrieving the details of a post from the database:

Быстрый

Note: This Firebase product is not available on the App Clip target.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Note: This Firebase product is not available on the App Clip target.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

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

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

Прочитайте данные один раз, используя метод getData().

The SDK is designed to manage interactions with database servers whether your app is online or offline.

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

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

The following example demonstrates retrieving a user's public-facing username a single time from the database:

Быстрый

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

Note: This Firebase product is not available on the App Clip target.
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;
}];

Unnecessary use of getData() can increase use of bandwidth and lead to loss of performance, which can be prevented by using a realtime listener as shown above.

Прочитайте данные один раз с помощью наблюдателя.

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

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

Быстрый

Note: This Firebase product is not available on the App Clip target.
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 синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами, используя принцип «максимальных усилий».

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

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

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

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