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

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

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

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

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

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

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

Получить FIRDatabaseReference

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

Быстрый

var ref: DatabaseReference!

ref = Database.database().reference()

Цель-C

@property (strong, nonatomic) FIRDatabaseReference *ref;

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

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

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

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

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

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

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

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

Быстрый

self.ref.child("users").child(user.uid).setValue(["username": username])

Цель-C

[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

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

Быстрый

self.ref.child("users/\(user.uid)/username").setValue(username)

Цель-C

[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

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

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

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

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

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

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

Быстрый

refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Цель-C

_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

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

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

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

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

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

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

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

Быстрый

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

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 , чтобы получить данные из кэша локального диска сразу.

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

Быстрый

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

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

Быстрый

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

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

Быстрый

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

[[[_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 на слушателя не автоматически удалять слушателей , зарегистрированных на его дочерних узлов; вы также должны отслеживать эти ссылки или маркеры, чтобы удалить их.

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

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

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

Быстрый

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

[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 , если его нет. Сервер сравнивает начальное значение с текущим значением и принимает транзакцию, если значения совпадают, или отклоняет ее. Если транзакция отклонена, сервер возвращает текущее значение клиенту, который снова запускает транзакцию с обновленным значением. Это повторяется до тех пор, пока транзакция не будет принята или не будет сделано слишком много попыток.

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

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

Быстрый

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

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

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

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

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

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