Odczyt i zapis danych na platformach Apple

(Opcjonalnie) Prototypowanie i testowanie w Pakiecie emulatorów lokalnych Firebase

Zanim porozmawiamy o tym, jak Twoja aplikacja odczytuje dane w Bazie danych czasu rzeczywistego i zapisuje je w tej bazie, omówię zestaw narzędzi, których możesz użyć do prototypowania i testowania funkcji Bazy danych czasu rzeczywistego: Pakiet emulatorów lokalnych Firebase. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły zabezpieczeń lub szukasz najbardziej ekonomicznego sposobu interakcji z backendem, możliwość pracy lokalnej bez wdrażania aktywnych usług może być świetnym pomysłem.

Emulator bazy danych czasu rzeczywistego jest częścią Pakietu emulatorów lokalnych, który umożliwia aplikacji interakcję z treścią i konfiguracją emulowanej bazy danych, a także opcjonalnie emulowanymi zasobami projektu (funkcjami, innymi bazami danych i regułami zabezpieczeń).

Aby użyć emulatora Bazy danych czasu rzeczywistego, wystarczy kilka kroków:

  1. Dodajesz wiersz kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
  2. Uruchomienie firebase emulators:start w katalogu głównym projektu lokalnego.
  3. Wywoływanie z prototypowego kodu aplikacji za pomocą pakietu SDK platformy Baza danych czasu rzeczywistego lub interfejsu API REST Bazy danych czasu rzeczywistego.

Dostępny jest szczegółowy instrukcja korzystania z Bazy danych czasu rzeczywistego i Cloud Functions. Zapoznaj się też z wprowadzeniem do Pakietu emulatorów lokalnych.

Pobieranie odniesienia do bazy danych FIRDatabase

Aby móc odczytywać lub zapisywać dane z bazy danych, potrzebujesz instancji FIRDatabaseReference:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
var ref: DatabaseReference!

ref = Database.database().reference()

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
@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 odbiornika asynchronicznego. Detektor jest aktywowany raz dla początkowego stanu danych i ponownie przy każdej zmianie danych.

Podstawowe operacje zapisu

W przypadku podstawowych operacji zapisu możesz użyć polecenia setValue, aby zapisać dane w określonym odwołaniu, zastępując wszystkie dotychczasowe dane w tej ścieżce. Możesz użyć tej metody, aby:

  • Typy kart odpowiadające dostępnym typom JSON:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Użytkownik z adresem setValue możesz np. dodać w ten sposób:

Swift

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

Objective-C

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

Użycie tego parametru setValue spowoduje zastąpienie danych w określonej lokalizacji, w tym wszystkich węzłów podrzędnych. Mimo to można zaktualizować element podrzędny bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie swoich profili, możesz zmienić nazwę użytkownika w ten sposób:

Swift

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

Objective-C

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Odczyt danych

Odczytuj dane przez nasłuchiwanie zdarzeń wartości

Aby odczytywać dane na ścieżce i nasłuchiwać zmian, użyj funkcji observeEventType:withBlock klasy FIRDatabaseReference do obserwowania zdarzeń FIRDataEventTypeValue.

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

Za pomocą zdarzenia FIRDataEventTypeValue możesz odczytać dane w danej ścieżce, które istnieją w momencie wystąpienia zdarzenia. Metoda ta jest wyzwalana raz, gdy podłączona jest detektor, i ponownie za każdym razem, gdy dane, w tym wszelkie elementy podrzędne, ulegną zmianie. Wywołanie zwrotne zdarzenia jest przekazywane w postaci snapshot zawierającej wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, zrzut zwróci wartość false, gdy wywołasz exists() i nil, gdy odczytasz jego właściwość value.

Ten przykład przedstawia aplikację do obsługi blogów społecznościowych, która pobiera szczegóły posta z bazy danych:

Swift

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

Objective-C

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

Detektor otrzymuje obiekt FIRDataSnapshot zawierający dane z określonej lokalizacji w bazie danych w chwili wystąpienia zdarzenia we właściwości value. Możesz je przypisać do odpowiedniego typu natywnego, np. NSDictionary. Jeśli w danej lokalizacji nie ma danych, value ma wartość nil.

Odczytaj dane raz

Odczyt raz za pomocą getData()

Pakiet SDK służy do zarządzania interakcjami z serwerami baz danych niezależnie od tego, czy aplikacja działa online, czy offline.

Ogólnie rzecz biorąc, do odczytywania danych i otrzymywania powiadomień o aktualizacjach danych z backendu należy używać opisanych powyżej technik dotyczących wartości zdarzeń. Techniki te zmniejszają wykorzystanie zasobów i płatności oraz są optymalizowane pod kątem wygody użytkowników korzystających z internetu i offline.

Jeśli dane są potrzebne tylko raz, możesz użyć funkcji getData(), aby uzyskać 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 jeśli nadal nie znajdzie wartości, zwróci błąd.

Ten przykład pokazuje jednorazowe pobranie publicznej nazwy użytkownika z bazy danych:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
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ć wykorzystanie przepustowości i spowodować utratę wydajności. Można temu zapobiec, korzystając z detektora w czasie rzeczywistym (jak pokazano powyżej).

Jednorazowe odczytywanie danych z obserwatorem

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej była zwracana natychmiast, zamiast sprawdzania aktualizacji na serwerze. W takich przypadkach możesz użyć polecenia observeSingleEventOfType, aby od razu pobrać dane z pamięci podręcznej dysku lokalnego.

Jest to przydatne w przypadku danych, które trzeba wczytać tylko raz i nie będą się często zmieniać ani wymagać aktywnego słuchania. Na przykład aplikacja do blogowania z poprzednich przykładów używa tej metody do wczytywania profilu użytkownika, gdy zaczyna pisać nowy post:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
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 lub usuwanie danych

Zaktualizuj określone pola

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.

Gdy wywołujesz updateChildValues, możesz zaktualizować wartości podrzędne niższego poziomu, podając ścieżkę klucza. Jeśli dane są przechowywane w kilku lokalizacjach w celu zwiększenia ich skalowania, możesz zaktualizować wszystkie ich wystąpienia za pomocą przekazywania danych na zewnątrz. Na przykład aplikacja do obsługi blogów społecznościowych może chcieć utworzyć posta i jednocześnie zaktualizować go do kanału ostatniej aktywności i kanału aktywności użytkownika publikującego. W tym celu aplikacja do tworzenia blogów używa takiego kodu:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
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 do utworzenia w węźle posta z postami dla wszystkich użytkowników w domenie /posts/$postid oraz jednoczesnego pobrania klucza za pomocą funkcji getKey(). Klucza można następnie użyć do utworzenia drugiego wpisu w postach użytkownika pod adresem /user-posts/$userid/$postid.

Za pomocą tych ścieżek możesz aktualizować wiele lokalizacji w drzewie JSON za pomocą jednego wywołania updateChildValues. Tak jak w tym przykładzie, w obu lokalizacjach. Aktualizacje wprowadzane w ten sposób są niepodzielne: albo wszystkie aktualizacje zakończą się sukcesem, albo wszystkie się kończą.

Dodaj blok ukończenia

Jeśli chcesz się dowiedzieć, kiedy dane zostały zatwierdzone, możesz dodać blok uzupełnienia. Zarówno setValue, jak i updateChildValues przyjmują opcjonalny blok ukończenia, który jest wywoływany po zatwierdzeniu zapisu w bazie danych. Przydaje się on do sprawdzania, które dane zostały zapisane, a które są nadal synchronizowane. Jeśli wywołanie się nie udało, detektor przekazuje obiekt błędu z informacją o przyczynie tego błędu.

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
[[[_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 usunięcia danych jest wywołanie metody removeValue w odniesieniu do lokalizacji tych danych.

Możesz też usunąć plik, określając nil jako wartość innej operacji zapisu, np. setValue lub updateChildValues. Możesz używać tej metody razem z funkcją updateChildValues, aby usunąć wiele elementów podrzędnych w jednym wywołaniu interfejsu API.

Odłącz detektory

Obserwatorzy nie zatrzymują automatycznie synchronizacji 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 element FIRDatabaseHandle do metody removeObserverWithHandle.

Gdy dodasz blok wywołania zwrotnego do pliku referencyjnego, zwrócony zostanie element FIRDatabaseHandle. Za pomocą tych uchwytów można usunąć blok wywołania zwrotnego.

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

Wywołanie removeObserverWithHandle lub removeAllObservers w detektorze nie powoduje automatycznego usunięcia detektorów zarejestrowanych w węzłach podrzędnych. Musisz też śledzić te odniesienia lub nicki, aby je usunąć.

Zapisywanie danych jako transakcji

Podczas pracy z danymi, które mogą ulec uszkodzeniu w wyniku równoczesnych modyfikacji, takich jak liczniki przyrostowe, możesz użyć operacji transakcji. Przyznajesz tej operacji 2 argumenty: funkcję aktualizacji i opcjonalne wywołanie zwrotne zakończenia. Funkcja aktualizacji przyjmuje bieżący stan danych jako argument i zwraca nowy pożądany stan do zapisania.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz umożliwić użytkownikom oznaczanie postów gwiazdką i usuwanie gwiazdek oraz śledzenie liczby przyznanych gwiazdek:

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
[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łowej liczbie gwiazdek, jeśli wielu użytkowników oznaczyło gwiazdką ten sam post w tym samym czasie lub klient miał nieaktualne dane. Wartość zawarta w klasie FIRMutableData jest początkowo ostatnią znaną wartością ścieżki klienta lub nil, jeśli jej nie ma. Serwer porównuje wartość początkową z bieżącą wartością i akceptuje transakcję, jeśli wartości są zgodne lub odrzuca. W przypadku odrzucenia transakcji serwer zwraca bieżącą wartość klientowi, który uruchamia transakcję ponownie ze zaktualizowaną wartością. Powtarza się, dopóki transakcja nie zostanie zaakceptowana lub nie zostanie podjęta zbyt wiele prób.

Atomic przyrosty po stronie serwera

W powyższym przypadku użycia zapisujemy w bazie danych 2 wartości: identyfikator użytkownika, który postawił gwiazdką lub usunął oznaczenie gwiazdką, oraz zwiększoną liczbę gwiazdek. Jeśli wiemy już, że użytkownik oznacza posta gwiazdką, możemy użyć niepodzielnej operacji przyrostu zamiast transakcji.

Swift

Uwaga: ta usługa Firebase nie jest dostępna w celu wycinka aplikacji.
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 celu wycinka aplikacji.
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 uruchamiany ponownie w przypadku wystąpienia konfliktu aktualizacji. Ponieważ jednak operacja zwiększania odbywa się bezpośrednio na serwerze bazy danych, nie ma ryzyka konfliktu.

Jeśli chcesz wykrywać i odrzucać konflikty dotyczące aplikacji (np. gdy użytkownik oznacza posta gwiazdką już wcześniej), musisz utworzyć niestandardowe reguły zabezpieczeń odpowiednie do tego zastosowania.

Praca z danymi w trybie 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 zachowuje własną, wewnętrzną wersję wszystkich aktywnych danych. Podczas zapisywania danych są one najpierw zapisywane w tej wersji lokalnej. Następnie klient Firebase synchronizuje te dane ze zdalnymi serwerami baz danych i innymi klientami w ramach najlepszych starań.

W rezultacie wszystkie zapisy w bazie danych natychmiast uruchamiają zdarzenia lokalne, zanim jakiekolwiek dane zostaną zapisane na serwerze. Oznacza to, że aplikacja pozostaje elastyczna bez względu na opóźnienia sieciowe czy połączenie.

Gdy połączenie zostanie przywrócone, aplikacja otrzyma odpowiedni zestaw zdarzeń, aby klient mógł synchronizować się z obecnym stanem serwera bez konieczności pisania niestandardowego kodu.

Więcej informacji o zachowaniu w trybie offline znajdziesz w artykule Więcej informacji o funkcjach online i offline.

Następne kroki