(Необязательно) Создание прототипов и тестирование с помощью 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 синхронизирует эти данные с удаленными серверами баз данных и другими клиентами по мере возможности.
В результате все записи в базу данных немедленно вызывают локальные события, прежде чем какие-либо данные будут записаны на сервер. Это означает, что ваше приложение остается отзывчивым независимо от задержки сети или подключения.
После восстановления подключения приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо пользовательский код.
Подробнее о работе в офлайн-режиме рассказывается в статье Возможности онлайн- и офлайн-режимов.
Дальнейшие действия
- Работа со списками данных
- Подробнее о структурированных данных…
- Подробнее о возможностях онлайн и офлайн…