(Opcjonalnie) Prototypowanie i testowanie za pomocą Firebase Local Emulator Suite
Zanim omówimy, jak Twoja aplikacja odczytuje i zapisuje dane w Realtime Database, przedstawimy zestaw narzędzi, których możesz używać do prototypowania i testowania funkcji Realtime Database: Firebase Local Emulator Suite. Jeśli testujesz różne modele danych, optymalizujesz reguły bezpieczeństwa lub szukasz najbardziej opłacalnego sposobu interakcji z backendem, praca lokalna bez wdrażania usług na żywo może być dobrym rozwiązaniem.
Emulator Realtime Database jest częścią Local Emulator Suite, który umożliwia aplikacji interakcję z emulowaną zawartością i konfiguracją bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami bezpieczeństwa).
Korzystanie z emulatora Realtime Database wymaga wykonania kilku czynności:
- Dodanie wiersza kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
- Uruchomienie polecenia
firebase emulators:startw katalogu głównym projektu lokalnego. - Wykonywanie wywołań z kodu prototypu aplikacji za pomocą pakietu SDK platformy Realtime Database jak zwykle lub za pomocą interfejsu API REST Realtime Database.
Dostępny jest szczegółowy przewodnik dotyczący Realtime Database i Cloud Functions. Zapoznaj się też z Local Emulator Suite wprowadzeniem.
Pobieranie elementu FIRDatabaseReference
Aby odczytywać i zapisywać dane w bazie danych, potrzebujesz instancji FIRDatabaseReference:
Swift
var ref: DatabaseReference! ref = Database.database().reference()
Objective-C
@property (strong, nonatomic) FIRDatabaseReference *ref; self.ref = [[FIRDatabase database] reference];
Zapisywanie danych
Ten dokument zawiera podstawowe informacje o odczytywaniu i zapisywaniu danych Firebase.
Dane Firebase są zapisywane w odniesieniu do Database i pobierane przez dołączenie do niego asynchronicznego detektora. Detektor jest aktywowany raz w przypadku stanu początkowego danych i ponownie za każdym razem, gdy dane się zmienią.
Podstawowe operacje zapisu
W przypadku podstawowych operacji zapisu możesz użyć setValue, aby zapisać dane w określonym odniesieniu, zastępując wszystkie istniejące dane w tej ścieżce. Za pomocą tej metody możesz:
- Przekazywać typy odpowiadające dostępnym typom JSON w ten sposób:
NSStringNSNumberNSDictionaryNSArray
Możesz na przykład dodać użytkownika za pomocą setValue w ten sposób:
Swift
self.ref.child("users").child(user.uid).setValue(["username": username])
Objective-C
[[[self.ref child:@"users"] child:authResult.user.uid] setValue:@{@"username": username}];
Użycie setValue w ten sposób powoduje zastąpienie danych w określonej lokalizacji, w tym wszystkich węzłów podrzędnych. Możesz jednak zaktualizować element podrzędny bez ponownego zapisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie profili, możesz zaktualizować nazwę użytkownika w ten sposób:
Swift
self.ref.child("users/\(user.uid)/username").setValue(username)
Objective-C
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];
Odczytywanie danych
Odczytywanie danych przez nasłuchiwanie zdarzeń wartości
Aby odczytać dane w ścieżce i nasłuchiwać zmian, użyj observeEventType:withBlock w FIRDatabaseReference, aby obserwować zdarzenia FIRDataEventTypeValue.
| Typ zdarzenia | Typowe zastosowanie |
|---|---|
FIRDataEventTypeValue |
Odczytywanie i nasłuchiwanie zmian w całej zawartości ścieżki. |
Za pomocą zdarzenia FIRDataEventTypeValue możesz odczytać dane w danej ścieżce w momencie wystąpienia zdarzenia. Ta metoda jest wywoływana raz po dołączeniu detektora i ponownie za każdym razem, gdy zmienią się dane, w tym elementy podrzędne. Wywołanie zwrotne zdarzenia otrzymuje snapshot zawierający wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, po wywołaniu exists() zrzut zwróci false, a po odczytaniu właściwości value zwróci nil.
Poniższy przykład pokazuje aplikację do blogowania społecznościowego, która pobiera szczegóły posta z bazy danych:
Swift
refHandle = postRef.observe(DataEventType.value, with: { snapshot in // ... })
Objective-C
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) { NSDictionary *postDict = snapshot.value; // ... }];
Detektor otrzymuje FIRDataSnapshot, który zawiera dane w określonej lokalizacji w bazie danych w momencie wystąpienia zdarzenia we właściwości value. Możesz przypisać wartości do odpowiedniego typu natywnego, np. NSDictionary.
Jeśli w danej lokalizacji nie ma danych, value ma wartość nil.
Jednorazowe odczytywanie danych
Jednorazowe odczytywanie danych za pomocą getData()
Pakiet SDK jest przeznaczony do zarządzania interakcjami z serwerami baz danych, niezależnie od tego, czy aplikacja jest online czy offline.
Zasadniczo do odczytywania danych należy używać opisanych powyżej technik zdarzeń wartości, aby otrzymywać powiadomienia o aktualizacjach danych z backendu. Te techniki zmniejszają zużycie i koszty oraz są zoptymalizowane pod kątem zapewnienia użytkownikom jak najlepszych wrażeń podczas przechodzenia do trybu online i offline.
Jeśli potrzebujesz danych tylko raz, możesz użyć getData(), aby pobrać zrzut danych z bazy danych. Jeśli z jakiegoś powodu getData() nie może zwrócić wartości serwera, klient sprawdzi pamięć podręczną pamięci lokalnej i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.
Poniższy przykład pokazuje jednorazowe pobieranie z bazy danych publicznej nazwy użytkownika:
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; }];
Niepotrzebne użycie getData() może zwiększyć zużycie przepustowości i spowodować spadek wydajności. Można temu zapobiec, używając detektora czasu rzeczywistego, jak pokazano powyżej.
Jednorazowe odczytywanie danych za pomocą obserwatora
W niektórych przypadkach możesz chcieć, aby wartość z pamięci podręcznej była zwracana natychmiast, zamiast sprawdzać zaktualizowaną wartość na serwerze. W takich przypadkach możesz użyć observeSingleEventOfType, aby natychmiast pobrać dane z pamięci podręcznej dysku lokalnego.
Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i które nie powinny się często zmieniać ani wymagać aktywnego nasłuchiwania. Na przykład aplikacja do blogowania z poprzednich przykładów używa tej metody do wczytywania profilu użytkownika, gdy zaczyna on tworzyć nowy post:
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); }];
Aktualizowanie i usuwanie danych
Aktualizowanie określonych pól
Aby jednocześnie zapisywać dane w określonych elementach podrzędnych węzła bez zastępowania innych węzłów podrzędnych, użyj metody updateChildValues.
Podczas wywoływania updateChildValues możesz aktualizować wartości elementów podrzędnych niższego poziomu, określając ścieżkę klucza. Jeśli dane są przechowywane w wielu lokalizacjach, aby lepiej skalować
, możesz zaktualizować wszystkie instancje tych danych za pomocą
zwielokrotnienia wyjściowego danych. Na przykład aplikacja do blogowania społecznościowego może chcieć utworzyć post i jednocześnie zaktualizować go w kanale ostatniej aktywności oraz w kanale aktywności użytkownika publikującego. Aby to zrobić, aplikacja do blogowania używa kodu takiego jak ten:
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];
Ten przykład używa childByAutoId, aby utworzyć post w węźle zawierającym posty wszystkich użytkowników w /posts/$postid i jednocześnie pobrać klucz za pomocą getKey(). Klucz można następnie użyć do utworzenia drugiego wpisu w postach użytkownika w /user-posts/$userid/$postid.
Za pomocą tych ścieżek możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania updateChildValues, np. tak jak w tym przykładzie, który tworzy nowy post w obu lokalizacjach. Jednoczesne aktualizacje wykonane w ten sposób są niepodzielne: wszystkie aktualizacje się powiodą lub wszystkie się nie powiodą.
Dodawanie bloku zakończenia
Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać blok zakończenia. Zarówno setValue, jak i updateChildValues przyjmują opcjonalny blok zakończenia, który jest wywoływany, gdy zapis zostanie zatwierdzony w bazie danych. Ten detektor może być przydatny do śledzenia, które dane zostały zapisane, a które są nadal synchronizowane. Jeśli wywołanie się nie powiodło, detektor otrzymuje obiekt błędu wskazujący przyczynę niepowodzenia.
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."); } }];
Usuwanie danych
Najprostszym sposobem usunięcia danych jest wywołanie removeValue w odniesieniu do lokalizacji tych danych.
Możesz też usunąć dane, określając nil jako wartość innej operacji zapisu, np. setValue lub updateChildValues. Za pomocą tej techniki
możesz usunąć wiele elementów podrzędnych za pomocą jednego wywołania interfejsu API.updateChildValues
Odłączanie detektorów
Obserwatorzy nie przestają automatycznie synchronizować danych, gdy opuścisz ViewController. Jeśli obserwator nie zostanie prawidłowo usunięty, będzie nadal synchronizować dane z pamięcią lokalną. Gdy obserwator nie jest już potrzebny, usuń go, przekazując powiązany FIRDatabaseHandle do metody removeObserverWithHandle.
Gdy dodasz blok wywołania zwrotnego do odniesienia, zostanie zwrócony FIRDatabaseHandle.
Te uchwyty można użyć do usunięcia bloku wywołania zwrotnego.
Jeśli do odniesienia do bazy danych dodano wiele detektorów, każdy z nich jest wywoływany, gdy wystąpi zdarzenie. Aby zatrzymać synchronizację danych w tej lokalizacji, musisz usunąć wszystkie obserwatory w tej lokalizacji, wywołując metodę removeAllObservers.
Wywołanie removeObserverWithHandle lub removeAllObservers w detektorze nie powoduje automatycznego usunięcia detektorów zarejestrowanych w jego węzłach podrzędnych. Musisz też śledzić te odniesienia lub uchwyty, aby je usunąć.
Zapisywanie danych jako transakcji
Podczas pracy z danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, np. liczniki przyrostowe, możesz użyć operacji transakcji. Podajesz w niej 2 argumenty: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy stan, który chcesz zapisać.
Na przykład w aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie postów gwiazdką i usuwanie gwiazdki oraz śledzenie liczby gwiazdek, które otrzymał post, w ten sposób:
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); } }];
Użycie transakcji zapobiega nieprawidłowym zliczaniu gwiazdek, jeśli wielu użytkowników jednocześnie oznaczy ten sam post gwiazdką lub jeśli klient miał nieaktualne dane. Wartość zawarta w klasie FIRMutableData jest początkowo ostatnią znaną klientowi wartością ścieżki lub nil, jeśli nie ma żadnej wartości. Serwer porównuje wartość początkową z bieżącą i akceptuje transakcję, jeśli wartości są zgodne, lub odrzuca ją. Jeśli transakcja zostanie odrzucona, serwer zwraca bieżącą wartość do klienta, który ponownie uruchamia transakcję z zaktualizowaną wartością. Powtarza się to, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie wykonana zbyt duża liczba prób.
Niepodzielne przyrosty po stronie serwera
W powyższym przypadku użycia zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który oznaczył post gwiazdką lub usunął gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznaczył post gwiazdką, możemy użyć operacji niepodzielnego przyrostu zamiast transakcji.
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];
Ten kod nie używa operacji transakcji, więc nie jest automatycznie ponownie uruchamiany w przypadku konfliktu aktualizacji. Ponieważ jednak operacja przyrostu odbywa się bezpośrednio na serwerze bazy danych, nie ma ryzyka konfliktu.
Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, np. gdy użytkownik oznaczy gwiazdką post, który już wcześniej oznaczył, musisz napisać niestandardowe reguły bezpieczeństwa dla tego przypadku użycia.
Praca z danymi offline
Jeśli klient utraci połączenie z siecią, Twoja aplikacja będzie nadal działać prawidłowo.
Każdy klient połączony z bazą danych Firebase utrzymuje własną wewnętrzną wersję aktywnych danych. Gdy dane są zapisywane, najpierw są zapisywane w tej wersji lokalnej. Klient Firebase synchronizuje te dane z serwerami zdalnej bazy danych i innymi klientami na zasadzie „najlepszych starań”.
W rezultacie wszystkie zapisy w bazie danych natychmiast wywołują zdarzenia lokalne, zanim jakiekolwiek dane zostaną zapisane na serwerze. Oznacza to, że Twoja aplikacja pozostaje responsywna niezależnie od opóźnienia sieci lub połączenia.
Po ponownym nawiązaniu połączenia aplikacja otrzymuje odpowiedni zestaw zdarzeń, dzięki czemu klient synchronizuje się z bieżącym stanem serwera bez konieczności pisania niestandardowego kodu.
Więcej informacji o działaniu offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.