(Необязательно) Создайте прототип и протестируйте его с помощью 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 .
Получить ссылку на базу данных FIR
Для чтения или записи данных из базы данных вам понадобится экземпляр 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()
не может вернуть значение сервера, клиент проверит кэш локального хранилища и вернет ошибку, если значение все еще не найдено.
В следующем примере показано однократное получение общедоступного имени пользователя из базы данных:
Быстрый
do { let snapshot = try await ref.child("users/\(uid)/username").getData() let userName = snapshot.value as? String ?? "Unknown" } catch { print(error) }
Цель-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
принимают необязательный блок завершения, который вызывается, когда запись зафиксирована в базе данных. Этот прослушиватель может быть полезен для отслеживания того, какие данные были сохранены, а какие еще синхронизируются. Если вызов оказался неудачным, прослушивателю передается объект ошибки, указывающий, почему произошел сбой.
Быстрый
do { try await ref.child("users").child(user.uid).setValue(["username": username]) print("Data saved successfully!") } catch { print("Data could not be saved: \(error).") }
Цель-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 синхронизирует эти данные с удаленными серверами баз данных и с другими клиентами по принципу «максимально возможно».
В результате все записи в базу данных немедленно вызывают локальные события, прежде чем какие-либо данные будут записаны на сервер. Это означает, что ваше приложение остается отзывчивым независимо от задержки в сети или подключения.
После восстановления подключения ваше приложение получает соответствующий набор событий, чтобы клиент синхронизировался с текущим состоянием сервера без необходимости писать какой-либо собственный код.
Мы поговорим подробнее о поведении в автономном режиме в разделе Узнайте больше о возможностях онлайн и офлайн .