Mobiele codeerders maken al jaren gebruik van het Mobile Backend as a Service (MBaaS)-platform Firebase Realtime Database van Google, waardoor ze zich kunnen concentreren op het bouwen van functies voor hun apps zonder zich zorgen te hoeven maken over de back-endinfrastructuur en database. Door het gemakkelijk te maken om gegevens in de cloud op te slaan en te bewaren en voor authenticatie en beveiliging te zorgen, stelt Firebase codeurs in staat zich te concentreren op de clientzijde.
Vorig jaar kondigde Google nog een andere back-end database-oplossing aan, Cloud Firestore, vanaf de grond opgebouwd met de belofte van grotere schaalbaarheid en intuïtiviteit. Dit zorgde echter voor enige verwarring over de plaats ervan in relatie tot het reeds bestaande vlaggenschipproduct van Google, Firebase Realtime Database. Deze zelfstudie schetst de verschillen tussen de twee platforms en de duidelijke voordelen van elk. U leert hoe u met Firestore-documentreferenties kunt werken en hoe u gegevens in realtime kunt lezen, schrijven, bijwerken en verwijderen door een eenvoudige app voor herinneringen te bouwen.
Doelstellingen van deze zelfstudie
In deze zelfstudie maakt u kennis met Cloud Firestore. U leert hoe u het platform kunt gebruiken voor realtime databasepersistentie en synchronisatie. We behandelen de volgende onderwerpen:
- wat Cloud Firestore is
- het Firestore-gegevensmodel
- Cloud Firestore instellen
- het maken van en werken met Cloud Firestore-referenties
- gegevens in realtime lezen uit Cloud Firestore
- gegevens maken, bijwerken en verwijderen
- filtering en samengestelde zoekopdrachten
Veronderstelde kennis
Deze tutorial gaat ervan uit dat je enige ervaring hebt gehad met Firebase en een achtergrond hebt ontwikkeld met Swift en Xcode.
Wat is Cloud Firestore?
Net als Firebase Realtime Database biedt Firestore mobiele en webontwikkelaars een platformonafhankelijke cloudoplossing om gegevens in realtime te bewaren, ongeacht netwerklatentie of internetverbinding, evenals naadloze integratie met de Google Cloud Platform-productsuite. Naast deze overeenkomsten zijn er duidelijke voor- en nadelen die de een van de ander onderscheiden.
Gegevensmodel
Op een fundamenteel niveau slaat Realtime Database gegevens op als één grote, monolithische, hiërarchische JSON-boom, terwijl Firestore gegevens organiseert in documenten en verzamelingen, evenals subverzamelingen. Dit vereist minder denormalisatie. Het opslaan van gegevens in één JSON-structuur heeft de voordelen van eenvoud als het gaat om het werken met eenvoudige gegevensvereisten; het wordt echter omslachtiger op schaal bij het werken met complexere hiërarchische gegevens.
Offline ondersteuning
Beide producten bieden offline ondersteuning, waarbij gegevens actief in wachtrijen worden opgeslagen wanneer er latente of geen netwerkconnectiviteit is, waarbij lokale wijzigingen waar mogelijk worden teruggestuurd naar de backend. Firestore ondersteunt naast mobiele apps ook offline synchronisatie voor web-apps, terwijl de Realtime Database alleen mobiele synchronisatie mogelijk maakt.
Vragen en transacties
Realtime Database ondersteunt slechts beperkte sorteer- en filtermogelijkheden:u kunt alleen sorteren of filteren op eigenschapsniveau, maar niet beide, in één enkele query. Query's zijn ook diep, wat betekent dat ze een grote subboom met resultaten teruggeven. Het product ondersteunt alleen eenvoudige schrijf- en transactiebewerkingen waarvoor een callback is vereist.
Firestore introduceert daarentegen indexquery's met samengestelde sortering en filtering, zodat u acties kunt combineren om kettingfilters en sortering te maken. U kunt ook ondiepe query's uitvoeren die subverzamelingen retourneren in plaats van de volledige verzameling die u zou krijgen met Realtime Database. Transacties zijn atomair van aard, of u nu een batchbewerking of enkelvoudige handeling verzendt, waarbij transacties automatisch worden herhaald totdat ze zijn afgerond. Bovendien ondersteunt Realtime Database alleen individuele schrijftransacties, terwijl Firestore batchbewerkingen atomair mogelijk maakt.
Prestaties en schaalbaarheid
De Realtime Database is, zoals je zou verwachten, behoorlijk robuust en heeft een lage latentie. Databases zijn echter beperkt tot afzonderlijke regio's, afhankelijk van zonale beschikbaarheid. Firestore daarentegen herbergt gegevens horizontaal over meerdere zones en regio's om echte wereldwijde beschikbaarheid, schaalbaarheid en betrouwbaarheid te garanderen. Google heeft zelfs beloofd dat Firestore betrouwbaarder zal zijn dan Realtime Database.
Een andere tekortkoming van de Realtime Database is de beperking tot 100.000 gelijktijdige gebruikers (100.000 gelijktijdige verbindingen en 1.000 schrijfbewerkingen/seconde in een enkele database), waarna u uw database zou moeten sharden (uw database opsplitsen in meerdere databases) om meer gebruikers te ondersteunen . Firestore schaalt automatisch over meerdere instanties zonder dat u hoeft in te grijpen.
Firestore is vanaf het begin ontworpen met schaalbaarheid in het achterhoofd en heeft een nieuwe schematische architectuur die gegevens repliceert over meerdere regio's, zorgt voor authenticatie en andere beveiligingsgerelateerde zaken afhandelt, allemaal binnen de SDK aan de clientzijde. Het nieuwe datamodel is intuïtiever dan dat van Firebase en lijkt meer op andere vergelijkbare NoSQL-databaseoplossingen zoals MongoDB, terwijl het een robuustere query-engine biedt.
Beveiliging
Ten slotte beheert Realtime Database, zoals u weet uit onze vorige tutorials, de beveiliging door middel van trapsgewijze regels met afzonderlijke validatietriggers. Dit werkt met Firebase-databaseregels, waarbij uw gegevens afzonderlijk worden gevalideerd. Firestore biedt daarentegen een eenvoudiger maar krachtiger beveiligingsmodel dat profiteert van Cloud Firestore-beveiligingsregels en identiteits- en toegangsbeheer (IAM), waarbij gegevensvalidatie automatisch wordt uitgesloten.
- Mobiele ontwikkelingFirebase-beveiligingsregelsChike Mgbemena
Het Firestore-gegevensmodel
Firestore is een op documenten gebaseerde NoSQL-database, bestaande uit verzamelingen documenten, die elk gegevens bevatten. Omdat het een NoSQL-database is, krijgt u geen tabellen, rijen en andere elementen die u in een relationele database zou vinden, maar in plaats daarvan sets sleutel/waarde-paren die u in documenten zou vinden.
Documenten en collecties maak je impliciet aan door gegevens aan een document toe te kennen, en als het document of de collectie niet bestaat, wordt deze automatisch voor je aangemaakt, aangezien de collectie altijd de root (eerste) node moet zijn. Hier is een eenvoudig Taken-voorbeeldschema van het project waaraan u binnenkort zult werken, bestaande uit de Taken-verzameling, evenals talrijke documenten met twee velden, de naam (string) en een vlag om aan te geven of de taak is voltooid (boolean) .
Laten we elk van de elementen ontleden, zodat u ze beter kunt begrijpen.
Collecties
Synoniem met databasetabellen in de SQL-wereld, collecties bevatten een of meer documenten. Collecties moeten de hoofdelementen in uw schema zijn en mogen alleen documenten bevatten, geen andere collecties. U kunt echter verwijzen naar een document dat op zijn beurt verwijst naar collecties (subcollecties).
In het bovenstaande diagram bestaat een taak uit twee primitieve velden (name en done) en een subverzameling (subtaak) die uit twee eigen primitieve velden bestaat.
Documenten
Documenten bestaan uit sleutel/waarde-paren, waarbij de waarden een van de volgende typen hebben:
- primitieve velden (zoals strings, getallen, boolean)
- complexe geneste objecten (lijsten of arrays van primitieven)
- subcollecties
Geneste objecten worden ook kaarten genoemd en kunnen als volgt in het document worden weergegeven. Het volgende is een voorbeeld van respectievelijk een genest object en array:
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"] ...
Raadpleeg de documentatie over gegevenstypen van Google voor meer informatie over de ondersteunde gegevenstypen. Vervolgens ga je een project opzetten om met Cloud Firestore te werken.
Het project opzetten
Als je al eerder met Firebase hebt gewerkt, zal veel hiervan je bekend voorkomen. Anders moet u een account maken in Firebase en de instructies volgen in het gedeelte 'Het project instellen' van onze vorige zelfstudie, Aan de slag met Firebase-verificatie voor iOS.
Als u deze zelfstudie wilt volgen, kloont u de repository van het zelfstudieproject. Voeg vervolgens de Firestore-bibliotheek toe door het volgende toevoegen aan je podbestand :
pod 'Firebase/Core' pod 'Firebase/Firestore'
Voer het volgende in uw terminal in om uw bibliotheek te bouwen:
pod install
Schakel vervolgens over naar Xcode en open de .xcworkspace het dossier. Navigeer naar de AppDelegate.swift bestand en voer het volgende in in de application:didFinishLaunchingWithOptions:
methode:
FirebaseApp.configure()
Ga in uw browser naar de Firebase-console en selecteer de Database tabblad aan de linkerkant.
Zorg ervoor dat u de optie selecteert om Starten in testmodus zodat u geen beveiligingsproblemen ondervindt terwijl we experimenteren, en let op de beveiligingsmelding wanneer u uw app in productie neemt. U bent nu klaar om een verzameling en enkele voorbeelddocumenten te maken.
Een verzameling en voorbeelddocument toevoegen
Maak om te beginnen een eerste verzameling, Tasks
, door de optie Collectie toevoegen . te selecteren knop en de collectie een naam te geven, zoals hieronder geïllustreerd:
Voor het eerste document laat u de document-ID leeg, waardoor automatisch een ID voor u wordt gegenereerd. Het document bestaat simpelweg uit twee velden: name
en done
.
Sla het document op en u zou de verzameling en het document samen met de automatisch gegenereerde ID moeten kunnen bevestigen:
Met de database opgezet met een voorbeelddocument in de cloud, bent u klaar om de Firestore SDK in Xcode te implementeren.
Databasereferenties maken en ermee werken
Open de MasterViewController.swift bestand in Xcode en voeg de volgende regels toe om de bibliotheek te importeren:
import Firebase class MasterViewController: UITableViewController { @IBOutlet weak var addButton: UIBarButtonItem! private var documents: [DocumentSnapshot] = [] public var tasks: [Task] = [] private var listener : ListenerRegistration! ...
Hier maakt u eenvoudig een listenervariabele waarmee u in realtime een verbinding met de database kunt activeren wanneer er een wijziging is. U maakt ook een DocumentSnapshot
referentie die de tijdelijke gegevenssnapshot bevat.
Maak voordat je verdergaat met de viewcontroller nog een Swift-bestand, Task.swift , die uw gegevensmodel vertegenwoordigt:
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) } }
Het bovenstaande codefragment bevat een gemakseigenschap (woordenboek) en methode (init) die het vullen van het modelobject gemakkelijker maken. Schakel terug naar de view-controller en declareer een globale setter-variabele die de basisquery beperkt tot de top 50 items in de takenlijst. U verwijdert ook de listener zodra u de queryvariabele instelt, zoals aangegeven in de didSet
eigendom hieronder:
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() }
Gegevens in realtime lezen vanuit Cloud Firestore
Met de documentverwijzing op zijn plaats, in viewWillAppear(_animated: Bool)
, koppelt u de listener die u eerder hebt gemaakt aan de resultaten van de momentopname van de query en haalt u een lijst met documenten op. Dit doet u door de Firestore-methode query?.addSnapshotListener
aan te roepen :
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() }
De sluiting hierboven wijst de snapshot.documents
. toe door de array iteratief toe te wijzen en deze in een nieuwe Task
. te plaatsen modelinstantieobject voor elk gegevensitem in de momentopname. Dus met slechts een paar regels heb je met succes alle taken uit de cloud ingelezen en toegewezen aan de globale tasks
reeks.
Vul het volgende in om de resultaten weer te geven TableView
methoden delegeren:
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 }
In dit stadium, bouw en voer het project uit en in de Simulator zou u in staat moeten zijn om gegevens in realtime te zien verschijnen. Voeg gegevens toe via de Firebase-console en u zou deze onmiddellijk in de app-simulator moeten zien verschijnen.
Gegevens maken, bijwerken en verwijderen
Nadat u de inhoud van de back-end met succes hebt gelezen, gaat u vervolgens gegevens maken, bijwerken en verwijderen. Het volgende voorbeeld illustreert hoe u gegevens kunt bijwerken, met behulp van een gekunsteld voorbeeld waarbij de app u alleen een item als voltooid laat markeren door op de cel te tikken. Let op de collection.document(
item.id
).updateData(["done": !item.done])
sluitingseigenschap, die eenvoudig verwijst naar een specifiek document-ID, waarbij elk van de velden in het woordenboek wordt bijgewerkt:
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) }
Om een item te verwijderen, roept u het document(
item.id
).delete()
methode:
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() } }
Als u een nieuwe taak maakt, moet u een nieuwe knop aan uw Storyboard toevoegen en de IBAction
ervan verbinden naar de view-controller, door een addTask(_ sender:)
. aan te maken methode. Wanneer een gebruiker op de knop drukt, verschijnt er een waarschuwingsblad waar de gebruiker een nieuwe taaknaam kan toevoegen:
collection("Tasks").addDocument (data: ["name": textFieldReminder.text ?? "empty task", "done": false])
Voltooi het laatste deel van de app door het volgende in te voeren:
@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) }
Bouw en voer de app nog een keer uit en probeer, wanneer de simulator verschijnt, een paar taken toe te voegen en een paar als voltooid te markeren, en test ten slotte de verwijderfunctie door enkele taken te verwijderen. U kunt bevestigen dat de opgeslagen gegevens in realtime zijn bijgewerkt door over te schakelen naar uw Firebase-databaseconsole en de verzameling en documenten te observeren.
Filteren en samengestelde zoekopdrachten
Tot nu toe heeft u alleen met een eenvoudige query gewerkt, zonder specifieke filtermogelijkheden. Om iets robuustere zoekopdrachten te maken, kunt u filteren op specifieke waarden door gebruik te maken van een whereField
clausule:
docRef.whereField(“name”, isEqualTo: searchString)
U kunt uw zoekopdrachtgegevens bestellen en beperken door gebruik te maken van de order(by: )
en limit(to: )
methoden als volgt:
docRef.order(by: "name").limit(5)
In de FirebaseDo-app heb je al gebruik gemaakt van limit
met de basisquery. In het bovenstaande fragment heb je ook gebruik gemaakt van een andere functie, samengestelde zoekopdrachten, waarbij zowel de volgorde als de limiet aan elkaar zijn gekoppeld. U kunt zoveel zoekopdrachten koppelen als u wilt, zoals in het volgende voorbeeld:
docRef .whereField(“name”, isEqualTo: searchString) .whereField(“done”, isEqualTo: false) .order(by: "name") .limit(5)