Odczytywanie i zapisywanie danych na platformach Apple

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

Zanim omówimy sposób, w jaki Twoja aplikacja odczytuje i zapisuje w Bazie danych czasu rzeczywistego, przedstawmy zestaw narzędzi, których możesz użyć do prototypowania i testowania funkcjonalności Bazy danych czasu rzeczywistego: Pakiet emulatorów lokalnych Firebase. Jeśli wypróbowujesz różne modele danych, optymalizujesz reguły zabezpieczeń lub pracujesz nad znalezieniem najbardziej opłacalnego sposobu interakcji z zapleczem, możliwość pracy lokalnej bez wdrażania usług na żywo może być świetnym pomysłem.

Emulator bazy danych czasu rzeczywistego jest częścią lokalnego pakietu emulatorów, który umożliwia aplikacji interakcję z zawartością i konfiguracją emulowanej bazy danych, a także opcjonalnie z emulowanymi zasobami projektu (funkcje, inne bazy danych i reguły zabezpieczeń).

Korzystanie z emulatora bazy danych czasu rzeczywistego obejmuje tylko kilka kroków:

  1. Dodanie wiersza kodu do konfiguracji testowej aplikacji, aby połączyć się z emulatorem.
  2. Z katalogu głównego lokalnego katalogu projektu uruchom firebase emulators:start .
  3. Wykonywanie wywołań z kodu prototypu aplikacji przy użyciu zestawu SDK platformy Realtime Database jak zwykle lub przy użyciu interfejsu API REST Realtime Database.

Dostępny jest szczegółowy przewodnik dotyczący Bazy danych czasu rzeczywistego i funkcji w chmurze . Powinieneś także rzucić okiem na wprowadzenie Local Emulator Suite .

Uzyskaj referencję bazy danych FIR

Do odczytu lub zapisu danych z bazy potrzebna jest instancja FIRDatabaseReference :

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
var ref: DatabaseReference!

ref = Database.database().reference()

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
@property (strong, nonatomic) FIRDatabaseReference *ref;

self.ref = [[FIRDatabase database] reference];

Zapisz dane

W tym dokumencie omówiono podstawy odczytywania i zapisywania danych Firebase.

Dane Firebase są zapisywane w odwołaniu do Database i pobierane przez dołączenie do niego asynchronicznego detektora. Odbiornik jest wyzwalany raz dla początkowego stanu danych i ponownie za każdym razem, gdy dane się zmieniają.

Podstawowe operacje zapisu

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

  • Przekaż typy odpowiadające dostępnym typom JSON w następujący sposób:
    • NSString
    • NSNumber
    • NSDictionary
    • NSArray

Na przykład możesz dodać użytkownika z setValue w następujący sposób:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
self.ref.child("users").child(user.uid).setValue(["username": username])

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
[[[self.ref child:@"users"] child:authResult.user.uid]
    setValue:@{@"username": username}];

Użycie setValue w ten sposób powoduje nadpisanie danych w określonej lokalizacji, w tym wszelkich węzłach podrzędnych. Jednak nadal możesz zaktualizować dziecko bez przepisywania całego obiektu. Jeśli chcesz zezwolić użytkownikom na aktualizowanie ich profili, możesz zaktualizować nazwę użytkownika w następujący sposób:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
self.ref.child("users/\(user.uid)/username").setValue(username)

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
[[[[_ref child:@"users"] child:user.uid] child:@"username"] setValue:username];

Przeczytaj dane

Odczytaj dane, nasłuchując zdarzeń wartości

Aby odczytać dane na ścieżce i nasłuchiwać zmian, użyj parametru observeEventType:withBlock w FIRDatabaseReference , aby obserwować zdarzenia FIRDataEventTypeValue .

Typ wydarzenia Typowe zastosowanie
FIRDataEventTypeValue Czytaj i słuchaj zmian w całej zawartości ścieżki.

Możesz użyć zdarzenia FIRDataEventTypeValue , aby odczytać dane w podanej ścieżce, ponieważ istnieje w momencie zdarzenia. Ta metoda jest wywoływana raz, gdy odbiornik jest podłączony, i ponownie za każdym razem, gdy dane, w tym wszelkie elementy podrzędne, ulegną zmianie. Do wywołania zwrotnego zdarzenia przekazuje się snapshot zawierającą wszystkie dane w tej lokalizacji, w tym dane podrzędne. Jeśli nie ma danych, migawka zwróci false , gdy wywołasz exists() i nil , gdy odczytasz jego właściwość value .

Poniższy przykład ilustruje aplikację do blogowania społecznościowego pobierającą szczegóły wpisu z bazy danych:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
refHandle = postRef.observe(DataEventType.value, with: { snapshot in
  // ...
})

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
_refHandle = [_postRef observeEventType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot * _Nonnull snapshot) {
  NSDictionary *postDict = snapshot.value;
  // ...
}];

Odbiornik odbiera FIRDataSnapshot , który zawiera dane w określonej lokalizacji w bazie danych w czasie 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 jest nil .

Przeczytaj dane raz

Przeczytaj raz za pomocą getData()

Zestaw SDK jest przeznaczony do zarządzania interakcjami z serwerami baz danych, niezależnie od tego, czy Twoja aplikacja jest online, czy offline.

Ogólnie rzecz biorąc, należy użyć opisanych powyżej technik zdarzeń wartości, aby odczytać dane i otrzymywać powiadomienia o aktualizacjach danych z zaplecza. Techniki te zmniejszają zużycie i opłaty oraz są zoptymalizowane, aby zapewnić użytkownikom jak najlepsze wrażenia podczas korzystania z Internetu i offline.

Jeśli potrzebujesz danych tylko raz, możesz użyć getData() , aby uzyskać migawkę danych z bazy danych. Jeśli z jakiegoś powodu getData() nie może zwrócić wartości serwera, klient przeszuka lokalną pamięć podręczną i zwróci błąd, jeśli wartość nadal nie zostanie znaleziona.

Poniższy przykład ilustruje jednorazowe pobieranie z bazy danych nazwy użytkownika dostępnej publicznie:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
ref.child("users/\(uid)/username").getData(completion:  { error, snapshot in
  guard error == nil else {
    print(error!.localizedDescription)
    return;
  }
  let userName = snapshot.value as? String ?? "Unknown";
});

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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 funkcji getData() może zwiększyć wykorzystanie przepustowości i doprowadzić do utraty wydajności, której można zapobiec, korzystając z programu nasłuchującego w czasie rzeczywistym, jak pokazano powyżej.

Przeczytaj dane raz z obserwatorem

W niektórych przypadkach możesz chcieć, aby wartość z lokalnej pamięci podręcznej została zwrócona natychmiast, zamiast sprawdzania zaktualizowanej wartości na serwerze. W takich przypadkach można użyć observeSingleEventOfType , aby natychmiast pobrać dane z pamięci podręcznej dysku lokalnego.

Jest to przydatne w przypadku danych, które trzeba załadować tylko raz i nie oczekuje się, że będą się często zmieniać lub wymagają aktywnego nasłuchiwania. Na przykład aplikacja do blogowania w poprzednich przykładach używa tej metody do ładowania profilu użytkownika, gdy rozpoczyna on tworzenie nowego posta:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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)
}

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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);
}];

Aktualizacja lub usuwanie danych

Zaktualizuj określone pola

Aby jednocześnie pisać do określonych elementów podrzędnych węzła bez nadpisywania innych węzłów podrzędnych, użyj metody updateChildValues .

Wywołując updateChildValues , możesz zaktualizować wartości podrzędne niższego poziomu, określając ścieżkę klucza. Jeśli dane są przechowywane w wielu lokalizacjach w celu lepszego skalowania, możesz zaktualizować wszystkie wystąpienia tych danych za pomocą funkcji rozdzielania danych . Na przykład aplikacja do blogowania społecznościowego może chcieć utworzyć post i jednocześnie zaktualizować go do kanału ostatniej aktywności i kanału aktywności użytkownika publikującego. Aby to zrobić, aplikacja do blogowania używa takiego kodu:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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)

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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];

Ten przykład używa childByAutoId do utworzenia wpisu w węźle zawierającym wpisy dla wszystkich użytkowników w /posts/$postid i jednoczesnego pobrania klucza za pomocą getKey() . Klucz może być następnie użyty do utworzenia drugiego wpisu we wpisach użytkownika w /user-posts/$userid/$postid .

Korzystając z tych ścieżek, możesz wykonywać jednoczesne aktualizacje w wielu lokalizacjach w drzewie JSON za pomocą jednego wywołania updateChildValues , na przykład w jaki sposób ten przykład tworzy nowy wpis w obu lokalizacjach. Aktualizacje symultaniczne wykonane w ten sposób są niepodzielne: albo wszystkie aktualizacje się powiodą, albo wszystkie aktualizacje kończą się niepowodzeniem.

Dodaj blok dokończenia

Jeśli chcesz wiedzieć, kiedy Twoje dane zostały zatwierdzone, możesz dodać blok uzupełniania. Zarówno setValue , jak i updateChildValues przyjmują opcjonalny blok uzupełniania, który jest wywoływany po zatwierdzeniu zapisu 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 nie powiodło się, obiektowi nasłuchującemu jest przekazywany obiekt błędu wskazujący przyczynę niepowodzenia.

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu aplikacji.
ref.child("users").child(user.uid).setValue(["username": username]) {
  (error:Error?, ref:DatabaseReference) in
  if let error = error {
    print("Data could not be saved: \(error).")
  } else {
    print("Data saved successfully!")
  }
}

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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.");
  }
}];

Usunąć dane

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

Możesz również usunąć, określając nil jako wartość dla innej operacji zapisu, takiej jak 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łącz słuchaczy

Obserwatorzy nie przestają automatycznie synchronizować danych, gdy opuszczasz ViewController . Jeśli obserwator nie zostanie poprawnie usunięty, kontynuuje synchronizację danych z pamięcią lokalną. Gdy obserwator nie jest już potrzebny, usuń go, przekazując skojarzony FIRDatabaseHandle do metody removeObserverWithHandle .

Po dodaniu bloku wywołania zwrotnego do odwołania zwracany jest 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 detektor jest wywoływany, gdy zostanie zgłoszone zdarzenie. Aby zatrzymać synchronizację danych w tej lokalizacji, musisz usunąć wszystkich obserwatorów w tej lokalizacji, wywołując metodę removeAllObservers .

Wywołanie removeObserverWithHandle lub removeAllObservers na odbiorniku nie usuwa automatycznie odbiorników zarejestrowanych w jego węzłach podrzędnych; musisz także śledzić te odniesienia lub uchwyty, aby je usunąć.

Zapisz dane jako transakcje

Podczas pracy z danymi, które mogą zostać uszkodzone przez współbieżne modyfikacje, takie jak liczniki przyrostowe, można użyć operacji transakcyjnej . Podajesz tej operacji dwa 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, który chcesz zapisać.

Na przykład w przykładowej aplikacji do blogowania społecznościowego możesz zezwolić użytkownikom na oznaczanie i odznaczanie postów oraz śledzenie liczby gwiazdek, które otrzymał post w następujący sposób:

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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)
  }
}

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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);
  }
}];

Korzystanie z transakcji zapobiega błędnym zliczaniu gwiazdek, jeśli wielu użytkowników oznacza ten sam post w tym samym czasie lub klient ma 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 wartością bieżącą i akceptuje transakcję, jeśli wartości są zgodne, lub ją odrzuca. W przypadku odrzucenia transakcji serwer zwraca bieżącą wartość klientowi, który ponownie uruchamia transakcję z wartością zaktualizowaną. Powtarza się to do momentu zaakceptowania transakcji lub wykonania zbyt wielu prób.

Atomowe przyrosty po stronie serwera

W powyższym przypadku zapisujemy do bazy danych dwie wartości: identyfikator użytkownika, który oznacza post gwiazdką/usuwa gwiazdkę, oraz zwiększoną liczbę gwiazdek. Jeśli już wiemy, że użytkownik jest gwiazdą posta, zamiast transakcji możemy użyć atomowej operacji inkrementacji.

Szybki

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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);

Cel C

Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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 konfliktu aktualizacji. Jednak ponieważ operacja inkrementacji odbywa się bezpośrednio na serwerze bazy danych, nie ma szans na konflikt.

Jeśli chcesz wykrywać i odrzucać konflikty specyficzne dla aplikacji, takie jak użytkownik oznaczony gwiazdką posta, który już wcześniej oznaczył, powinieneś napisać niestandardowe reguły bezpieczeństwa dla tego przypadku użycia.

Pracuj z danymi w trybie offline

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

Każdy klient połączony z bazą danych Firebase utrzymuje własną wewnętrzną wersję wszystkich aktywnych danych. Kiedy dane są zapisywane, są najpierw zapisywane w tej lokalnej wersji. Klient Firebase synchronizuje następnie te dane ze zdalnymi serwerami baz danych oraz z innymi klientami na zasadzie „najlepszych starań”.

W rezultacie wszystkie zapisy w bazie danych natychmiast wyzwalają zdarzenia lokalne, zanim jakiekolwiek dane zostaną zapisane na serwerze. Oznacza to, że Twoja aplikacja pozostaje responsywna bez względu na opóźnienie sieci lub łączność.

Po ponownym nawiązaniu łączności aplikacja otrzymuje odpowiedni zestaw zdarzeń, dzięki czemu klient synchronizuje się z bieżącym stanem serwera bez konieczności pisania niestandardowego kodu.

Więcej o zachowaniu w trybie offline omówimy w artykule Dowiedz się więcej o możliwościach trybu online i offline .

Następne kroki