Programiści mobilni od wielu lat korzystają z platformy Google Mobile Backend as a Service (MBaaS) bazy danych czasu rzeczywistego Firebase, pomagając im skupić się na tworzeniu funkcji dla swoich aplikacji bez martwienia się o infrastrukturę zaplecza i bazę danych. Ułatwiając przechowywanie i utrwalanie danych w chmurze oraz dbając o uwierzytelnianie i bezpieczeństwo, Firebase pozwala programistom skupić się na stronie klienta.
W zeszłym roku Google ogłosiło kolejne rozwiązanie bazodanowe zaplecza, Cloud Firestore, zbudowane od podstaw z obietnicą większej skalowalności i intuicyjności. Wprowadziło to jednak pewne zamieszanie co do jego miejsca w stosunku do już istniejącego flagowego produktu Google, Firebase Realtime Database. W tym samouczku zostaną przedstawione różnice między tymi dwiema platformami i różne zalety każdej z nich. Dowiesz się, jak pracować z odniesieniami do dokumentów Firestore, a także czytać, zapisywać, aktualizować i usuwać dane w czasie rzeczywistym, budując prostą aplikację przypominającą.
Cele tego samouczka
W tym samouczku poznasz Cloud Firestore. Dowiesz się, jak wykorzystać platformę do utrwalania i synchronizacji bazy danych w czasie rzeczywistym. Omówimy następujące tematy:
- czym jest Cloud Firestore
- model danych Firestore
- konfigurowanie Cloud Firestore
- tworzenie i praca z materiałami referencyjnymi Cloud Firestore
- odczytywanie danych w czasie rzeczywistym z Cloud Firestore
- tworzenie, aktualizacja i usuwanie danych
- filtrowanie i zapytania złożone
Założona wiedza
W tym samouczku założono, że miałeś kontakt z Firebase i masz doświadczenie w programowaniu za pomocą Swift i Xcode.
Co to jest Cloud Firestore?
Podobnie jak baza danych czasu rzeczywistego Firebase, Firestore zapewnia deweloperom aplikacji mobilnych i internetowych wieloplatformowe rozwiązanie chmurowe do utrwalania danych w czasie rzeczywistym, niezależnie od opóźnień w sieci lub połączenia internetowego, a także bezproblemową integrację z pakietem produktów Google Cloud Platform. Wraz z tymi podobieństwami istnieją wyraźne zalety i wady, które odróżniają jedno od drugiego.
Model danych
Na podstawowym poziomie Realtime Database przechowuje dane jako jedno duże, monolityczne, hierarchiczne drzewo JSON, podczas gdy Firestore organizuje dane w dokumentach i kolekcjach, a także podkolekcjach. Wymaga to mniejszej denormalizacji. Przechowywanie danych w jednym drzewie JSON ma zalety prostoty, jeśli chodzi o pracę z prostymi wymaganiami dotyczącymi danych; jednak staje się bardziej uciążliwe na dużą skalę podczas pracy z bardziej złożonymi danymi hierarchicznymi.
Wsparcie offline
Oba produkty oferują obsługę trybu offline, aktywnie buforując dane w kolejkach, gdy istnieje ukryta lub brak łączności sieciowej — w miarę możliwości synchronizując lokalne zmiany z powrotem do zaplecza. Firestore obsługuje synchronizację offline dla aplikacji internetowych oprócz aplikacji mobilnych, podczas gdy Baza danych czasu rzeczywistego umożliwia tylko synchronizację mobilną.
Zapytania i transakcje
Baza danych czasu rzeczywistego obsługuje tylko ograniczone możliwości sortowania i filtrowania — w jednym zapytaniu można sortować lub filtrować tylko na poziomie właściwości, ale nie jedno i drugie. Zapytania są również głębokie, co oznacza, że zwracają duże poddrzewo wyników. Produkt obsługuje tylko proste operacje zapisu i transakcji, które wymagają wywołania zwrotnego zakończenia.
Firestore natomiast wprowadza zapytania indeksowe ze złożonym sortowaniem i filtrowaniem, co pozwala łączyć akcje w celu tworzenia filtrów łańcuchowych i sortowania. Możesz także wykonywać płytkie zapytania zwracając podkolekcje zamiast całej kolekcji, którą otrzymasz dzięki Bazie danych czasu rzeczywistego. Transakcje mają charakter atomowy, niezależnie od tego, czy wysyłasz operację zbiorczą, czy pojedynczą, a transakcje powtarzają się automatycznie do momentu ich zawarcia. Ponadto baza danych czasu rzeczywistego obsługuje tylko pojedyncze transakcje zapisu, podczas gdy Firestore zapewnia atomowe operacje wsadowe.
Wydajność i skalowalność
Baza danych czasu rzeczywistego, jak można się spodziewać, jest dość solidna i ma niskie opóźnienia. Jednak bazy danych są ograniczone do pojedynczych regionów, w zależności od dostępności strefowej. Z drugiej strony Firestore przechowuje dane poziomo w wielu strefach i regionach, aby zapewnić prawdziwą globalną dostępność, skalowalność i niezawodność. W rzeczywistości Google obiecał, że Firestore będzie bardziej niezawodny niż Baza danych czasu rzeczywistego.
Inną wadą Bazy Danych Czasu Rzeczywistego jest ograniczenie do 100 000 jednoczesnych użytkowników (100 000 jednoczesnych połączeń i 1000 zapisów/sekundę w jednej bazie danych), po którym musiałbyś podzielić swoją bazę danych na fragmenty (podzielić bazę danych na wiele baz danych) w celu obsługi większej liczby użytkowników . Firestore automatycznie skaluje się w wielu instancjach bez konieczności interwencji.
Zaprojektowany od podstaw z myślą o skalowalności, Firestore ma nową architekturę schematów, która replikuje dane w wielu regionach, dba o uwierzytelnianie i zajmuje się innymi kwestiami związanymi z bezpieczeństwem, a wszystko to w ramach swojego pakietu SDK po stronie klienta. Jego nowy model danych jest bardziej intuicyjny niż model Firebase, bardziej przypomina inne porównywalne rozwiązania baz danych NoSQL, takie jak MongoDB, a jednocześnie zapewnia bardziej niezawodny silnik zapytań.
Bezpieczeństwo
Wreszcie baza danych czasu rzeczywistego, jak wiecie z naszych poprzednich samouczków, zarządza bezpieczeństwem poprzez kaskadowe reguły z oddzielnymi wyzwalaczami walidacji. Działa to z regułami bazy danych Firebase i osobno weryfikuje dane. Z drugiej strony Firestore zapewnia prostszy, ale bardziej wydajny model zabezpieczeń, wykorzystujący reguły zabezpieczeń Cloud Firestore oraz zarządzanie tożsamością i dostępem (IAM), z automatycznym wyłączaniem weryfikacji danych.
- Programowanie mobilne Zasady bezpieczeństwa Firebase Chike Mgbemena
Model danych Firestore
Firestore to oparta na dokumentach baza danych NoSQL, składająca się ze zbiorów dokumentów, z których każdy zawiera dane. Ponieważ jest to baza danych NoSQL, nie otrzymasz tabel, wierszy i innych elementów, które można znaleźć w relacyjnej bazie danych, ale zamiast tego zestawy par klucz/wartość, które można znaleźć w dokumentach.
Dokumenty i kolekcje tworzysz niejawnie, przypisując dane do dokumentu, a jeśli dokument lub kolekcja nie istnieje, zostanie automatycznie utworzona dla Ciebie, ponieważ kolekcja zawsze musi być węzłem głównym (pierwszym). Oto prosty przykładowy schemat Tasks projektu, nad którym wkrótce będziesz pracować, składający się z kolekcji Tasks, a także licznych dokumentów zawierających dwa pola, nazwę (ciąg znaków) oraz flagę informującą o wykonaniu zadania (wartość logiczna) .
Rozłóżmy każdy z elementów, abyś mógł je lepiej zrozumieć.
Kolekcje
Kolekcje, które są synonimem tabel baz danych w świecie SQL, zawierają jeden lub więcej dokumentów. Kolekcje muszą być elementami głównymi w schemacie i mogą zawierać tylko dokumenty, a nie inne kolekcje. Możesz jednak odwołać się do dokumentu, który z kolei odnosi się do kolekcji (podzbiorów).
Na powyższym diagramie zadanie składa się z dwóch podstawowych pól (nazwa i gotowe) oraz podzbioru (podzadanie), które składa się z dwóch własnych pól pierwotnych.
Dokumenty
Dokumenty składają się z par klucz/wartość, przy czym wartości mają jeden z następujących typów:
- pola pierwotne (takie jak ciągi, liczby, wartości logiczne)
- złożone obiekty zagnieżdżone (listy lub tablice prymitywów)
- kolekcje podrzędne
Obiekty zagnieżdżone są również nazywane mapami i mogą być reprezentowane w dokumencie w następujący sposób. Poniżej znajduje się przykład odpowiednio zagnieżdżonego obiektu i tablicy:
ID: 2422892 //primitive name: “Remember to buy milk” detail: //nested object notes: "This is a task to buy milk from the store" created: 2017-04-09 due: 2017-04-10 done: false notify: ["2F22-89R2", "L092-G623", "H00V-T4S1"] ...
Więcej informacji o obsługiwanych typach danych znajdziesz w dokumentacji Google dotyczącej typów danych. Następnie skonfigurujesz projekt do pracy z Cloud Firestore.
Konfigurowanie projektu
Jeśli pracowałeś wcześniej z Firebase, wiele z nich powinno być Ci znajome. W przeciwnym razie musisz utworzyć konto w Firebase i postępować zgodnie z instrukcjami w sekcji „Konfigurowanie projektu” w naszym poprzednim samouczku „Pierwsze kroki z uwierzytelnianiem Firebase w systemie iOS”.
Aby śledzić ten samouczek, sklonuj repozytorium projektu samouczka. Następnie dołącz bibliotekę Firestore od dodanie następujących elementów do Podfile :
pod 'Firebase/Core' pod 'Firebase/Firestore'
Wpisz w terminalu następujące informacje, aby zbudować bibliotekę:
pod install
Następnie przejdź do Xcode i otwórz .xcworkspace plik. Przejdź do AppDelegate.swift i wpisz następujące dane w application:didFinishLaunchingWithOptions:
metoda:
FirebaseApp.configure()
W przeglądarce przejdź do konsoli Firebase i wybierz Baza danych po lewej stronie.
Upewnij się, że wybrałeś opcję Uruchom w trybie testowym dzięki czemu podczas eksperymentów nie wystąpią żadne problemy z zabezpieczeniami, a po przeniesieniu aplikacji do środowiska produkcyjnego zwróć uwagę na ostrzeżenie dotyczące bezpieczeństwa. Jesteś teraz gotowy do utworzenia kolekcji i kilku przykładowych dokumentów.
Dodawanie kolekcji i przykładowego dokumentu
Aby rozpocząć, utwórz początkową kolekcję, Tasks
, wybierając Dodaj kolekcję przycisk i nazwanie kolekcji, jak pokazano poniżej:
W przypadku pierwszego dokumentu pozostawisz puste pole Identyfikator dokumentu, co spowoduje automatyczne wygenerowanie identyfikatora. Dokument będzie po prostu składał się z dwóch pól: name
i done
.
Zapisz dokument i powinieneś być w stanie potwierdzić odbiór i dokument wraz z automatycznie wygenerowanym identyfikatorem:
Po skonfigurowaniu bazy danych z przykładowym dokumentem w chmurze możesz rozpocząć wdrażanie pakietu Firestore SDK w Xcode.
Tworzenie i praca z odniesieniami do baz danych
Otwórz MasterViewController.swift plik w Xcode i dodaj następujące wiersze, aby zaimportować bibliotekę:
import Firebase class MasterViewController: UITableViewController { @IBOutlet weak var addButton: UIBarButtonItem! private var documents: [DocumentSnapshot] = [] public var tasks: [Task] = [] private var listener : ListenerRegistration! ...
Tutaj po prostu tworzysz zmienną nasłuchiwania, która pozwoli Ci wyzwolić połączenie z bazą danych w czasie rzeczywistym, gdy nastąpi zmiana. Tworzysz także DocumentSnapshot
odniesienie, które będzie przechowywać tymczasową migawkę danych.
Zanim przejdziesz dalej z kontrolerem widoku, utwórz kolejny szybki plik, Task.swift , który będzie reprezentował Twój model danych:
import Foundation struct Task{ var name:String var done: Bool var id: String var dictionary: [String: Any] { return [ "name": name, "done": done ] } } extension Task{ init?(dictionary: [String : Any], id: String) { guard let name = dictionary["name"] as? String, let done = dictionary["done"] as? Bool else { return nil } self.init(name: name, done: done, id: id) } }
Powyższy fragment kodu zawiera wygodną właściwość (słownik) i metodę (init), które ułatwią wypełnianie obiektu modelu. Przełącz się z powrotem do kontrolera widoku i zadeklaruj globalną zmienną ustawiającą, która ograniczy zapytanie podstawowe do pierwszych 50 wpisów na liście zadań. Odbiornik zostanie również usunięty po ustawieniu zmiennej zapytania, jak wskazano w didSet
właściwość poniżej:
fileprivate func baseQuery() -> Query { return Firestore.firestore().collection("Tasks").limit(to: 50) } fileprivate var query: Query? { didSet { if let listener = listener { listener.remove() } } } override func viewDidLoad() { super.viewDidLoad() self.query = baseQuery() } override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) self.listener.remove() }
Odczytywanie danych w czasie rzeczywistym z Cloud Firestore
Z odniesieniem do dokumentu na miejscu, w viewWillAppear(_animated: Bool)
, powiąż utworzony wcześniej detektor z wynikami migawki zapytania i pobierz listę dokumentów. Odbywa się to przez wywołanie metody Firestore query?.addSnapshotListener
:
self.listener = query?.addSnapshotListener { (documents, error) in guard let snapshot = documents else { print("Error fetching documents results: \(error!)") return } let results = snapshot.documents.map { (document) -> Task in if let task = Task(dictionary: document.data(), id: document.documentID) { return task } else { fatalError("Unable to initialize type \(Task.self) with dictionary \(document.data())") } } self.tasks = results self.documents = snapshot.documents self.tableView.reloadData() }
Powyższe zamknięcie przypisuje snapshot.documents
mapując tablicę iteracyjnie i owijając ją w nowe Task
obiekt instancji modelu dla każdego elementu danych w zrzucie. Tak więc za pomocą zaledwie kilku linijek pomyślnie odczytałeś wszystkie zadania z chmury i przypisałeś je do globalnych tasks
szyk.
Aby wyświetlić wyniki, wypełnij następujące TableView
metody delegowania:
override func numberOfSections(in tableView: UITableView) -> Int { return 1 } override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { return tasks.count } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let item = tasks[indexPath.row] cell.textLabel!.text = item.name cell.textLabel!.textColor = item.done == false ? UIColor.black : UIColor.lightGray return cell }
Na tym etapie zbuduj i uruchom projekt, a w Symulatorze powinieneś być w stanie obserwować dane pojawiające się w czasie rzeczywistym. Dodaj dane za pomocą konsoli Firebase, a powinny natychmiast pojawić się w symulatorze aplikacji.
Tworzenie, aktualizacja i usuwanie danych
Po pomyślnym odczytaniu treści z zaplecza, następnie będziesz tworzyć, aktualizować i usuwać dane. Następny przykład ilustruje, jak zaktualizować dane, używając wymyślonego przykładu, w którym aplikacja pozwoli tylko oznaczyć element jako wykonany, dotykając komórki. Zwróć uwagę na collection.document(
item.id
).updateData(["done": !item.done])
właściwość zamknięcia, która po prostu odwołuje się do określonego identyfikatora dokumentu, aktualizując każde z pól w słowniku:
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { let item = tasks[indexPath.row] let collection = Firestore.firestore().collection("Tasks") collection.document(item.id).updateData([ "done": !item.done, ]) { err in if let err = err { print("Error updating document: \(err)") } else { print("Document successfully updated") } } tableView.reloadRows(at: [indexPath], with: .automatic) }
Aby usunąć element, wywołaj document(
item.id
).delete()
metoda:
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { return true } override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCellEditingStyle, forRowAt indexPath: IndexPath) { if (editingStyle == .delete){ let item = tasks[indexPath.row] _ = Firestore.firestore().collection("Tasks").document(item.id).delete() } }
Utworzenie nowego zadania będzie wymagało dodania nowego przycisku w Storyboardzie i połączenia jego IBAction
do kontrolera widoku, tworząc addTask(_ sender:)
metoda. Gdy użytkownik naciśnie przycisk, wyświetli się arkusz alertów, w którym użytkownik może dodać nową nazwę zadania:
collection("Tasks").addDocument (data: ["name": textFieldReminder.text ?? "empty task", "done": false])
Uzupełnij ostatnią część aplikacji, wpisując:
@IBAction func addTask(_ sender: Any) { let alertVC : UIAlertController = UIAlertController(title: "New Task", message: "What do you want to remember?", preferredStyle: .alert) alertVC.addTextField { (UITextField) in } let cancelAction = UIAlertAction.init(title: "Cancel", style: .destructive, handler: nil) alertVC.addAction(cancelAction) //Alert action closure let addAction = UIAlertAction.init(title: "Add", style: .default) { (UIAlertAction) -> Void in let textFieldReminder = (alertVC.textFields?.first)! as UITextField let db = Firestore.firestore() var docRef: DocumentReference? = nil docRef = db.collection("Tasks").addDocument(data: [ "name": textFieldReminder.text ?? "empty task", "done": false ]) { err in if let err = err { print("Error adding document: \(err)") } else { print("Document added with ID: \(docRef!.documentID)") } } } alertVC.addAction(addAction) present(alertVC, animated: true, completion: nil) }
Zbuduj i uruchom aplikację jeszcze raz, a gdy pojawi się symulator, spróbuj dodać kilka zadań, a także oznaczyć kilka jako wykonanych, a na koniec przetestuj funkcję usuwania, usuwając niektóre zadania. Możesz potwierdzić, że przechowywane dane zostały zaktualizowane w czasie rzeczywistym, przełączając się na konsolę bazy danych Firebase i obserwując kolekcję i dokumenty.
Filtrowanie i zapytania złożone
Do tej pory pracowałeś tylko z prostym zapytaniem, bez żadnych konkretnych możliwości filtrowania. Aby utworzyć nieco bardziej niezawodne zapytania, możesz filtrować według określonych wartości, korzystając z whereField
klauzula:
docRef.whereField(“name”, isEqualTo: searchString)
Możesz uporządkować i ograniczyć dane zapytania, korzystając z order(by: )
i limit(to: )
metody w następujący sposób:
docRef.order(by: "name").limit(5)
W aplikacji FirebaseDo korzystałeś już z limit
z zapytaniem podstawowym. W powyższym fragmencie wykorzystałeś również inną funkcję, zapytania złożone, w których zarówno kolejność, jak i limit są ze sobą połączone. Możesz połączyć dowolną liczbę zapytań, tak jak w poniższym przykładzie:
docRef .whereField(“name”, isEqualTo: searchString) .whereField(“done”, isEqualTo: false) .order(by: "name") .limit(5)