Odczyt i zapis danych na platformach Apple

(Opcjonalnie) Prototypowanie i testowanie za pomocą Firebase Local Emulator Suite

Zanim omówimy, jak 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 zabezpieczeń lub szukasz najbardziej opłacalnego sposobu interakcji z backendem, możliwość pracy lokalnej bez wdrażania usług na żywo może być świetnym rozwiązaniem.

Emulator Realtime Database jest częścią Local Emulator Suite, która 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 zabezpieczeń).

Aby użyć emulatora Realtime Database, wystarczy wykonać kilka czynności:

  1. dodanie do konfiguracji testowej aplikacji wiersza kodu, który umożliwia połączenie z emulatorem;
  2. Uruchom firebase emulators:start w katalogu głównym projektu lokalnego.
  3. wykonywanie wywołań z kodu prototypu aplikacji przy użyciu pakietu SDK platformy Realtime Database w zwykły sposób lub przy użyciu interfejsu API typu REST Realtime Database;

Dostępny jest szczegółowy przewodnik dotyczący Realtime Database i Cloud Functions. Warto też zapoznać się z wprowadzeniem do Local Emulator Suite.

Pobieranie FIRDatabaseReference

Aby odczytywać dane z bazy danych lub je w niej zapisywać, potrzebujesz instancji FIRDatabaseReference:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
@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 odwołaniu Database i pobierane przez dołączenie do niego asynchronicznego detektora. Odbiornik jest aktywowany raz dla stanu początkowego danych i ponownie za każdym razem, gdy dane ulegną zmianie.

Podstawowe operacje zapisu

W przypadku podstawowych operacji zapisu możesz użyć metody setValue, aby zapisać dane w określonej referencji, zastępując wszystkie istniejące dane w tej ścieżce. Możesz użyć tej metody, aby:

  • Typy kart, które odpowiadają dostępnym typom JSON:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Możesz na przykład dodać użytkownika z setValue w ten sposób:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
self.ref.child("users").child(user.uid).setValue(["username": username])

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

Użycie metody 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 konieczności ponownego zapisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie profili, możesz zmienić nazwę użytkownika w ten sposób:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
self.ref.child("users/\(user.uid)/username").setValue(username)

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
[[[[_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 do obserwowania zdarzeń FIRDataEventTypeValue.

Typ zdarzenia Typowe zastosowanie
FIRDataEventTypeValue Odczytywanie i nasłuchiwanie zmian w całej zawartości ścieżki.

Możesz użyć zdarzenia FIRDataEventTypeValue, aby odczytać dane w określonej ścieżce, które istnieją w momencie wystąpienia zdarzenia. Ta metoda jest wywoływana raz, gdy detektor jest dołączony, i ponownie za każdym razem, gdy dane (w tym elementy podrzędne) ulegną zmianie. Do wywołania zwrotnego zdarzenia przekazywany jest obiekt snapshot zawierający wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, zrzut zwróci false, gdy wywołasz exists(), i nil, gdy odczytasz jego właściwość value.

Poniższy przykład pokazuje, jak aplikacja do blogowania społecznościowego pobiera szczegóły posta z bazy danych:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

Detektor otrzymuje obiekt 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, takiego jak NSDictionary. Jeśli w lokalizacji nie ma danych, value to nil.

Odczytywanie danych raz

Odczyt jednorazowy za pomocą funkcji 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 i otrzymywania powiadomień o aktualizacjach danych z backendu należy używać opisanych powyżej technik zdarzeń wartości. Te techniki zmniejszają wykorzystanie i opłaty oraz są zoptymalizowane pod kątem zapewnienia użytkownikom jak najlepszych wrażeń podczas przechodzenia między trybami online i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć getData(), aby uzyskać zrzut danych z bazy danych. Jeśli z jakiegoś powodu usługa 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, jak jednorazowo pobrać z bazy danych publiczną nazwę użytkownika:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
do {
  let snapshot = try await ref.child("users/\(uid)/username").getData()
  let userName = snapshot.value as? String ?? "Unknown"
} catch {
  print(error)
}

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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żywanie metody getData() może zwiększyć wykorzystanie przepustowości i prowadzić do spadku wydajności, czemu można zapobiec, używając detektora działającego w czasie rzeczywistym, jak pokazano powyżej.

Odczytywanie danych raz za pomocą obserwatora

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej była zwracana natychmiast, zamiast sprawdzać, czy na serwerze jest zaktualizowana wartość. W takich przypadkach możesz użyć observeSingleEventOfType, aby natychmiast pobrać dane z lokalnej pamięci podręcznej na dysku.

Jest to przydatne w przypadku danych, które muszą zostać wczytane tylko raz i nie powinny często się 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 pisać nowy post:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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 węzłach 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 podrzędne niższego poziomu, podając ścieżkę klucza. Jeśli dane są przechowywane w kilku lokalizacjach, aby zapewnić lepsze skalowanie, możesz zaktualizować wszystkie instancje tych danych za pomocą rozpowszechniania danych. Na przykład aplikacja do blogowania społecznościowego może utworzyć posta i jednocześnie zaktualizować go w kanale ostatniej aktywności oraz w kanale aktywności użytkownika, który go opublikował. W tym celu aplikacja do blogowania używa kodu podobnego do tego:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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];

W tym przykładzie użyto metody childByAutoId, aby utworzyć post w węźle zawierającym posty wszystkich użytkowników w /posts/$postid i jednocześnie pobrać klucz za pomocą metody getKey(). Klucz może być następnie użyty do utworzenia drugiego wpisu w postach użytkownika na stronie /user-posts/$userid/$postid.

Korzystając z tych ścieżek, możesz jednocześnie aktualizować wiele lokalizacji w drzewie JSON za pomocą pojedynczego wywołania updateChildValues, tak jak w tym przykładzie, w którym nowy post jest tworzony w obu lokalizacjach. Jednoczesne aktualizacje wprowadzane w ten sposób są niepodzielne: albo wszystkie aktualizacje się powiodą, albo wszystkie zakończą się niepowodzeniem.

Dodawanie bloku ukończenia

Jeśli chcesz wiedzieć, kiedy dane zostały zatwierdzone, możesz dodać blok ukoń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 powiedzie, do detektora zostanie przekazany obiekt błędu z informacją o przyczynie niepowodzenia.

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
[[[_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.");
  }
}];

Usuń dane

Najprostszym sposobem na usunięcie danych jest wywołanie metody removeValue w odniesieniu do lokalizacji tych danych.

Możesz też usunąć, podając nil jako wartość innej operacji zapisu, np. setValue lub updateChildValues. Możesz użyć tej techniki z updateChildValues, aby usunąć wiele elementów podrzędnych w jednym wywołaniu interfejsu API.

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 obiekt FIRDatabaseHandle do metody removeObserverWithHandle.

Gdy dodasz blok wywołania zwrotnego do odwołania, zwracany jest obiekt FIRDatabaseHandle. Te uchwyty mogą służyć do usuwania bloku wywołania zwrotnego.

Jeśli do odwołania do bazy danych dodano wiele detektorów, każdy z nich jest wywoływany po wystąpieniu zdarzenia. Aby zatrzymać synchronizowanie danych w tej lokalizacji, musisz usunąć wszystkich obserwatorów w lokalizacji, wywołując metodę removeAllObservers.

Wywołanie metody removeObserverWithHandle lub removeAllObservers na obiekcie nasłuchującym nie powoduje automatycznego usunięcia obiektów nasłuchujących zarejestrowanych w jego węzłach podrzędnych. Aby je usunąć, musisz też śledzić te odwołania lub uchwyty.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą zostać uszkodzone przez jednoczesne modyfikacje, np. licznikami przyrostowymi, możesz użyć operacji transakcji. Ta operacja wymaga 2 argumentów: funkcji aktualizacji i opcjonalnego wywołania zwrotnego po zakończeniu. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy pożądany stan, który chcesz zapisać.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz umożliwić użytkownikom oznaczanie i usuwanie oznaczeń gwiazdką przy postach oraz śledzenie liczby gwiazdek, które otrzymał post, w ten sposób:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
[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łowemu zliczaniu gwiazdek, gdy wielu użytkowników jednocześnie dodaje gwiazdkę do tego samego posta lub gdy klient ma nieaktualne dane. Wartość zawarta w klasie FIRMutableData jest początkowo ostatnią znaną klientowi wartością ścieżki lub nil, jeśli nie ma żadnej. Serwer porównuje wartość początkową z bieżącą i akceptuje transakcję, jeśli wartości są zgodne, lub ją odrzuca. Jeśli transakcja zostanie odrzucona, serwer zwraca klientowi bieżącą wartość, która ponownie uruchamia transakcję ze zaktualizowaną wartością. Proces ten powtarza się, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie przekroczona 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 dodaje post do ulubionych lub usuwa go z ulubionych, oraz zwiększoną liczbę gwiazdek. Jeśli już wiemy, że użytkownik oznaczył post gwiazdką, możemy użyć operacji przyrostu atomowego zamiast transakcji.

Swift

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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

Uwaga: ta usługa Firebase nie jest dostępna w przypadku celu App Clip.
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 zostanie automatycznie uruchomiony ponownie, jeśli wystąpi konflikt aktualizacji. Ponieważ jednak operacja inkrementacji odbywa się bezpośrednio na serwerze bazy danych, nie ma możliwości wystąpienia konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty związane z aplikacją, np. gdy użytkownik oznaczy gwiazdką post, który już wcześniej oznaczył gwiazdką, musisz napisać niestandardowe reguły zabezpieczeń dla tego przypadku użycia.

Praca z danymi offline

Jeśli klient utraci połączenie sieciowe, aplikacja będzie nadal działać prawidłowo.

Każdy klient połączony z bazą danych Firebase utrzymuje własną wewnętrzną wersję wszystkich aktywnych danych. Gdy dane są zapisywane, najpierw są zapisywane w tej wersji lokalnej. Klient Firebase synchronizuje te dane ze zdalnymi serwerami baz 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 aplikacja pozostaje responsywna niezależnie od opóźnień 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 zachowaniu offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.

Następne kroki