(Необязательно) Создание прототипов и тестирование с помощью Firebase Local Emulator Suite
Прежде чем говорить о том, как ваше приложение считывает и записывает данные в Realtime Database, давайте познакомимся с набором инструментов, которые можно использовать для создания прототипов и тестирования функций Realtime Database: Firebase Local Emulator Suite. Если вы пробуете разные модели данных, оптимизируете правила безопасности или ищете наиболее экономичный способ взаимодействия с серверной частью, возможность работать локально без развертывания действующих сервисов может быть отличной идеей.
Эмулятор Realtime Database входит в состав Local Emulator Suite. Он позволяет приложению взаимодействовать с эмулируемыми данными и конфигурацией базы данных, а также с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).
Чтобы начать работу с эмулятором Realtime Database, выполните следующие действия:
- Добавьте строку кода в тестовую конфигурацию приложения, чтобы подключиться к эмулятору.
- Выполните команду
firebase emulators:startв корневом каталоге локального проекта. - Вызовы из кода прототипа приложения с помощью SDK платформы Realtime Database или REST API Realtime Database.
Доступно подробное руководство по работе с Realtime Database и Cloud Functions. Также рекомендуем ознакомиться с вводной статьей о Local Emulator Suite.
Получите FIRDatabaseReference
Чтобы читать данные из базы данных или записывать их в нее, вам нужен экземпляр FIRDatabaseReference:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Запись данных
В этом документе рассматриваются основы чтения и записи данных Firebase.
Данные Firebase записываются в ссылку Database и извлекаются путем прикрепления к ней асинхронного прослушивателя. Прослушиватель срабатывает один раз для начального состояния данных и затем каждый раз, когда данные изменяются.
Основные операции записи
Для базовых операций записи можно использовать setValue, чтобы сохранить данные в указанной ссылке, заменив все существующие данные по этому пути. Этот метод можно использовать, чтобы:
- Типы карт, соответствующие доступным типам JSON:
NSStringNSNumberNSDictionaryNSArray
Например, вы можете добавить пользователя с помощью setValue следующим образом:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
При таком использовании setValue перезаписывает данные в указанном месте, включая все дочерние узлы. Однако вы можете обновить дочерний узел, не перезаписывая весь объект. Если вы хотите разрешить пользователям обновлять свои профили,
вы можете обновить имя пользователя следующим образом:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Чтение данных
Как читать данные, прослушивая события значений
Чтобы прочитать данные по пути и отслеживать изменения, используйте observeEventType:withBlock из FIRDatabaseReference для наблюдения за событиями FIRDataEventTypeValue.
| Тип события | Типичные сценарии использования |
|---|---|
FIRDataEventTypeValue |
Чтение и отслеживание изменений всего содержимого пути. |
Событие FIRDataEventTypeValue позволяет считывать данные по заданному пути в том виде, в котором они существуют на момент события. Этот метод запускается один раз при подключении прослушивателя и затем каждый раз, когда изменяются данные, включая дочерние элементы. Обратному вызову события передается snapshot, содержащий все данные в этом местоположении, включая дочерние данные. Если данных нет, то при вызове exists() снимок вернет false, а при чтении свойства value – nil.
В следующем примере показано, как приложение для ведения блогов извлекает из базы данных сведения о записи:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Прослушиватель получает объект FIRDataSnapshot, который содержит данные в указанном месте базы данных на момент события в свойстве value. Вы можете присвоить значения соответствующему нативному типу, например NSDictionary.
Если в указанном месте нет данных, value будет nil.
Прочитать данные один раз
Однократное чтение с помощью getData()
SDK предназначен для управления взаимодействием с серверами баз данных независимо от того, находится ли ваше приложение в сети или офлайн.
Как правило, для чтения данных и получения уведомлений об их обновлении из серверной части следует использовать описанные выше методы событий значений. Эти методы позволяют снизить объем использования и расходы, а также оптимизированы для обеспечения наилучшего взаимодействия с пользователями как в онлайн-, так и в офлайн-режиме.
Если данные нужны вам только один раз, вы можете использовать getData(), чтобы получить снимок данных из базы данных. Если по какой-либо причине getData() не может вернуть значение сервера, клиент проверит кеш локального хранилища и вернет ошибку, если значение по-прежнему не найдено.
В примере ниже показано, как получить из базы данных общедоступное имя пользователя.
Swift
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Objective-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, чтобы сразу получить данные из локального кеша диска.
Это полезно для данных, которые нужно загрузить только один раз и которые не будут часто меняться или требовать активного прослушивания. Например, приложение для ведения блогов из предыдущих примеров использует этот метод для загрузки профиля пользователя, когда он начинает создавать новую запись:
Swift
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
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 вы можете обновить значения дочерних элементов более низкого уровня, указав путь к ключу. Если данные хранятся в нескольких местах для лучшего масштабирования, вы можете обновить все экземпляры этих данных с помощью разветвления данных. Например, приложение для ведения блогов в соцсетях может создавать запись и одновременно обновлять ее в фиде недавних действий и фиде действий пользователя, создавшего запись. Для этого в приложении используется следующий код:
Swift
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
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) принимают необязательный блок завершения, который вызывается, когда запись зафиксирована в базе данных. Этот прослушиватель позволяет отслеживать, какие данные уже сохранены, а какие ещё синхронизируются. Если вызов не удался, прослушивателю передается объект ошибки с указанием причины сбоя.
Swift
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
[[[_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 для прослушивателя не приводит к автоматическому удалению прослушивателей, зарегистрированных на его дочерних узлах. Чтобы удалить их, вам также необходимо отслеживать эти ссылки или дескрипторы.
Сохранение данных в виде транзакций
При работе с данными, которые могут быть повреждены одновременными изменениями, например с инкрементными счетчиками, можно использовать транзакцию. Для этой операции нужно указать два аргумента: функцию обновления и необязательный обратный вызов завершения. Функция обновления принимает текущее состояние данных в качестве аргумента и возвращает новое желаемое состояние, которое вы хотите записать.
Например, в приложении для ведения блогов вы можете разрешить пользователям ставить и убирать отметки "Нравится" и отслеживать, сколько отметок получил пост, следующим образом:
Swift
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
[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, если его нет. Сервер сравнивает исходное значение с текущим и принимает транзакцию, если они совпадают, или отклоняет ее. Если транзакция отклонена, сервер возвращает клиенту текущее значение, и клиент снова выполняет транзакцию с обновленным значением. Это повторяется до тех пор, пока транзакция не будет принята или пока не будет сделано слишком много попыток.
Атомарные приращения на стороне сервера
В приведенном выше примере мы записываем в базу данных два значения: идентификатор пользователя, который ставит или убирает звездочку, и увеличенное количество звездочек. Если мы уже знаем, что пользователь пометил запись звездочкой, мы можем использовать атомарную операцию инкремента вместо транзакции.
Swift
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
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 синхронизирует эти данные с удаленными серверами баз данных и другими клиентами по мере возможности.
В результате все записи в базу данных немедленно вызывают локальные события, прежде чем какие-либо данные будут записаны на сервер. Это означает, что ваше приложение остается отзывчивым независимо от задержки сети или подключения.
После восстановления подключения приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо пользовательский код.
Подробнее о работе в офлайн-режиме рассказывается в статье Возможности онлайн- и офлайн-режимов.