Создание прототипа и тестирование с помощью Firebase Local Emulator Suite (необязательно)
Прежде чем мы поговорим о том, как ваше приложение может читать данные из Realtime Database и записывать их туда, давайте рассмотрим набор инструментов, которые можно использовать для создания прототипов и тестирования функций Realtime Database: Firebase Local Emulator Suite. Если вы тестируете разные модели данных, оптимизируете правила безопасности или ищете наиболее экономичный способ взаимодействия с серверной частью, возможность работать локально без развертывания сервисов может быть очень полезной.
Эмулятор Realtime Database входит в состав Local Emulator Suite и позволяет приложению взаимодействовать с эмулируемым контентом базы данных и конфигурацией, а также, при необходимости, с эмулируемыми ресурсами проекта (функциями, другими базами данных и правилами безопасности).
Чтобы использовать эмулятор Realtime Database, выполните следующие действия:
- Добавление строки кода в конфигурацию тестирования приложения для подключения к эмулятору.
- Выполните команду
firebase emulators:startв корневом каталоге локального проекта. - Вызовы из прототипа кода приложения с помощью Realtime Database SDK платформы или Realtime Database REST API.
Подробное пошаговое руководство по использованию 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 синхронизирует эти данные с удаленными серверами базы данных и другими клиентами по мере возможности.
В результате все записи в базу данных немедленно активируют локальные события, прежде чем какие-либо данные будут записаны на сервер. Это значит, что приложение будет работать без сбоев независимо от задержки сети или качества подключения.
После восстановления подключения приложение получит нужный набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо код.
Подробнее об офлайн-поведении рассказывается в разделе Возможности онлайн- и офлайн-рекламы.