Database
 sql >> Database >  >> RDS >> Database

Introduzione a Cloud Firestore per iOS

I programmatori mobili sfruttano da molti anni la piattaforma Mobile Backend as a Service (MBaaS) Firebase Realtime Database di Google, aiutandoli a concentrarsi sulla creazione di funzionalità per le loro app senza doversi preoccupare dell'infrastruttura e del database back-end. Semplificando l'archiviazione e la persistenza dei dati nel cloud e occupandosi dell'autenticazione e della sicurezza, Firebase consente ai programmatori di concentrarsi sul lato client.

L'anno scorso, Google ha annunciato l'ennesima soluzione di database back-end, Cloud Firestore, costruita da zero con la promessa di una maggiore scalabilità e intuitività. Tuttavia, ciò ha introdotto una certa confusione sul suo posto in relazione al prodotto di punta già esistente di Google, Firebase Realtime Database. Questo tutorial illustrerà le differenze tra le due piattaforme e i vantaggi distinti di ciascuna. Imparerai come lavorare con i riferimenti ai documenti Firestore, oltre a leggere, scrivere, aggiornare ed eliminare i dati in tempo reale, creando una semplice app di promemoria.

Obiettivi di questo tutorial

Questo tutorial ti esporrà a Cloud Firestore. Imparerai come sfruttare la piattaforma per la persistenza e la sincronizzazione del database in tempo reale. Tratteremo i seguenti argomenti:

  • che cos'è Cloud Firestore
  • il modello di dati Firestore
  • configurazione di Cloud Firestore
  • creare e lavorare con i riferimenti di Cloud Firestore
  • lettura dei dati in tempo reale da Cloud Firestore
  • creazione, aggiornamento ed eliminazione dei dati
  • filtro e query composte

Conoscenza presunta

Questo tutorial presuppone che tu abbia avuto una certa esposizione a Firebase e un background di sviluppo con Swift e Xcode.

Cos'è Cloud Firestore?

Come Firebase Realtime Database, Firestore offre agli sviluppatori di dispositivi mobili e Web una soluzione cloud multipiattaforma per mantenere i dati in tempo reale, indipendentemente dalla latenza di rete o dalla connettività Internet, oltre a una perfetta integrazione con la suite di prodotti Google Cloud Platform. Insieme a queste somiglianze, ci sono vantaggi e svantaggi distinti che differenziano l'uno dall'altro.

Modello di dati

A livello fondamentale, Realtime Database archivia i dati come un albero JSON grande, monolitico e gerarchico, mentre Firestore organizza i dati in documenti e raccolte, nonché in sottoraccolte. Ciò richiede una minore denormalizzazione. L'archiviazione dei dati in un albero JSON presenta i vantaggi della semplicità quando si tratta di lavorare con semplici requisiti di dati; tuttavia, diventa più ingombrante su larga scala quando si lavora con dati gerarchici più complessi.

Supporto offline

Entrambi i prodotti offrono supporto offline, memorizzando attivamente nella cache i dati nelle code in caso di connettività di rete latente o assente, sincronizzando le modifiche locali con il back-end quando possibile. Firestore supporta la sincronizzazione offline per le app Web oltre alle app mobili, mentre il database in tempo reale consente solo la sincronizzazione mobile.

Query e transazioni 

Il database in tempo reale supporta solo funzionalità di ordinamento e filtro limitate:puoi ordinare o filtrare solo a livello di proprietà, ma non entrambi, in una singola query. Anche le query sono profonde, nel senso che restituiscono un grande sottoalbero di risultati. Il prodotto supporta solo semplici operazioni di scrittura e transazione che richiedono una richiamata di completamento.

Firestore, d'altra parte, introduce query di indice con ordinamento e filtraggio composti, consentendo di combinare azioni per creare filtri e ordinamento a catena. Puoi anche eseguire query superficiali restituendo sottoraccolte al posto dell'intera raccolta che otterresti con Realtime Database. Le transazioni sono di natura atomica, indipendentemente dal fatto che si invii un'operazione batch o una singola, con transazioni che si ripetono automaticamente fino alla conclusione. Inoltre, Realtime Database supporta solo singole transazioni di scrittura, mentre Firestore consente operazioni batch in modo atomico.

Prestazioni e scalabilità

Il database in tempo reale, come ci si aspetterebbe, è abbastanza robusto e ha una bassa latenza. Tuttavia, i database sono limitati a singole regioni, in base alla disponibilità zonale. Firestore, d'altra parte, ospita i dati orizzontalmente in più zone e regioni per garantire disponibilità, scalabilità e affidabilità globali reali. In effetti, Google ha promesso che Firestore sarà più affidabile di Realtime Database.

Un'altra lacuna del database in tempo reale è la limitazione a 100.000 utenti simultanei (100.000 connessioni simultanee e 1.000 scritture al secondo in un singolo database) dopo di che dovresti dividere il tuo database (dividere il tuo database in più database) per supportare più utenti . Firestore si ridimensiona automaticamente su più istanze senza che tu debba intervenire.

Progettato da zero pensando alla scalabilità, Firestore ha una nuova architettura schematica che replica i dati in più regioni, si occupa dell'autenticazione e gestisce altre questioni relative alla sicurezza all'interno del suo SDK lato client. Il suo nuovo modello di dati è più intuitivo di quello di Firebase, più simile ad altre soluzioni di database NoSQL comparabili come MongoDB, fornendo al contempo un motore di query più robusto.

Sicurezza 

Infine, Realtime Database, come saprai dai nostri tutorial precedenti, gestisce la sicurezza attraverso regole a cascata con trigger di convalida separati. Funziona con le regole del database Firebase, convalidando i tuoi dati separatamente. Firestore, d'altra parte, fornisce un modello di sicurezza più semplice ma più potente che sfrutta le regole di sicurezza e la gestione dell'identità e dell'accesso (IAM) di Cloud Firestore, con la convalida dei dati esclusa automaticamente.

  • Sviluppo mobile Regole di sicurezza FirebaseChike Mgbemena

Il modello di dati Firestore

Firestore è un database basato su documenti NoSQL, costituito da raccolte di documenti, ognuno dei quali contiene dati. Poiché si tratta di un database NoSQL, non otterrai tabelle, righe e altri elementi che potresti trovare in un database relazionale, ma invece set di coppie chiave/valore che potresti trovare all'interno dei documenti.

Crei documenti e raccolte in modo implicito assegnando dati a un documento e, se il documento o la raccolta non esiste, verrà automaticamente creato per te, poiché la raccolta deve sempre essere il (primo) nodo radice. Ecco un semplice schema di esempio di Tasks del progetto su cui lavorerai a breve, composto dalla raccolta Tasks, oltre a numerosi documenti contenenti due campi, il nome (stringa) e un flag per indicare se l'attività è stata completata (booleano) .

Scomponiamo ciascuno degli elementi in modo da poterli comprendere meglio.

Raccolte

Sinonimo di tabelle di database nel mondo SQL, le raccolte contengono uno o più documenti. Le raccolte devono essere gli elementi principali nello schema e possono contenere solo documenti, non altre raccolte. Tuttavia, puoi fare riferimento a un documento che a sua volta fa riferimento a raccolte (sottoraccolte).

Nel diagramma sopra, un'attività è composta da due campi primitivi (nome e fatto) e da una sotto-raccolta (attività secondaria) che consiste in due campi primitivi propri.

Documenti

I documenti sono costituiti da coppie chiave/valore, con i valori di uno dei seguenti tipi: 

  • campi primitivi (come stringhe, numeri, booleano)
  • oggetti nidificati complessi (elenchi o array di primitive)
  • sottoraccolte

Gli oggetti nidificati sono anche chiamati mappe e possono essere rappresentati come segue, all'interno del documento. Quello che segue è un esempio rispettivamente di un oggetto nidificato e di un 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"]
...

Per ulteriori informazioni sui tipi di dati supportati, fai riferimento alla documentazione Tipi di dati di Google. Successivamente, imposterai un progetto per funzionare con Cloud Firestore.

Impostazione del progetto

Se hai già lavorato con Firebase, molto di questo dovrebbe esserti familiare. In caso contrario, dovrai creare un account in Firebase e seguire le istruzioni nella sezione "Impostazione del progetto" del nostro tutorial precedente, Introduzione all'autenticazione Firebase per iOS .

Per seguire questo tutorial, clona il repository del progetto del tutorial. Successivamente, includi la libreria Firestore di aggiungendo quanto segue al tuo  Podfile :

pod 'Firebase/Core' 
pod 'Firebase/Firestore'

Inserisci quanto segue nel tuo terminale per creare la tua libreria:

pod install

Quindi, passa a Xcode e apri il  .xcworkspace file. Vai a AppDelegate.swift  e inserisci quanto segue all'interno di application:didFinishLaunchingWithOptions: metodo:

FirebaseApp.configure()

Nel browser, vai alla Firebase Console e seleziona il Database scheda a sinistra.

Assicurati di selezionare l'opzione per Avvia in modalità test in modo da non avere problemi di sicurezza mentre sperimentiamo e prestare attenzione all'avviso di sicurezza quando sposti la tua app in produzione. Ora sei pronto per creare una raccolta e alcuni documenti di esempio.

Aggiunta di una raccolta e di un documento campione

Per iniziare, crea una raccolta iniziale, Tasks , selezionando Aggiungi raccolta pulsante e nominando la collezione, come illustrato di seguito:

Per il primo documento, lascerai vuoto l'ID documento, che genererà automaticamente un ID per te. Il documento sarà semplicemente composto da due campi: namedone .

Salva il documento e dovresti essere in grado di confermare la raccolta e il documento insieme all'ID generato automaticamente:

Con il database configurato con un documento di esempio nel cloud, sei pronto per iniziare a implementare l'SDK Firestore in Xcode.

Creazione e utilizzo dei riferimenti al database

Apri il MasterViewController.swift file in Xcode e aggiungi le seguenti righe per importare la libreria:

import Firebase

class MasterViewController: UITableViewController {
    @IBOutlet weak var addButton: UIBarButtonItem!
    
    private var documents: [DocumentSnapshot] = []
    public var tasks: [Task] = []
    private var listener : ListenerRegistration!
   ...

Qui stai semplicemente creando una variabile listener che ti permetterà di attivare una connessione al database in tempo reale quando c'è una modifica. Stai anche creando un DocumentSnapshot riferimento che conterrà l'istantanea dei dati temporanei.

Prima di continuare con il controller di visualizzazione, crea un altro file swift, Task.swift , che rappresenterà il tuo modello di dati:

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)
    }
}

Il frammento di codice sopra include una proprietà di convenienza (dizionario) e un metodo (init) che renderanno più semplice il popolamento dell'oggetto modello. Tornare al controller di visualizzazione e dichiarare una variabile setter globale che vincolerà la query di base alle prime 50 voci nell'elenco delle attività. Rimuoverai anche il listener dopo aver impostato la variabile di query, come indicato in didSet proprietà sottostante:

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()
    }

Lettura dei dati in tempo reale da Cloud Firestore

Con il riferimento al documento in atto, in viewWillAppear(_animated: Bool) , associa il listener creato in precedenza ai risultati dello snapshot della query e recupera un elenco di documenti. Questo viene fatto chiamando il metodo 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()
            
        }

La chiusura sopra assegna il snapshot.documents mappando l'array in modo iterativo e avvolgendolo in una nuova Tasks oggetto istanza del modello per ogni elemento di dati nello snapshot. Quindi, con poche righe, hai letto con successo tutte le attività dal cloud e le hai assegnate alle tasks globali   Vettore.

Per visualizzare i risultati, compila i seguenti TableView metodi delegati:

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
    }

A questo punto, costruisci ed esegui il progetto e nel simulatore dovresti essere in grado di osservare i dati che appaiono in tempo reale. Aggiungi i dati tramite la console Firebase e dovresti vederli apparire istantaneamente nel simulatore di app.

Creazione, aggiornamento ed eliminazione dei dati

Dopo aver letto con successo il contenuto dal back-end, creerai, aggiornerai ed eliminerai i dati. L'esempio successivo illustrerà come aggiornare i dati, usando un esempio inventato in cui l'app ti consentirà solo di contrassegnare un elemento come completato toccando la cella. Nota il collection.document( item.id ).updateData(["done": !item.done]) proprietà di chiusura, che fa semplicemente riferimento a un ID documento specifico, aggiornando ciascuno dei campi nel dizionario:

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)
        
    }

Per eliminare un elemento, chiama il document( item.id ).delete() metodo:

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()
        }

    }

La creazione di una nuova attività comporterà l'aggiunta di un nuovo pulsante nello Storyboard e il collegamento della sua IBAction al controller di visualizzazione, creando un addTask(_ sender:) metodo. Quando un utente preme il pulsante, verrà visualizzato un foglio di avviso in cui l'utente può aggiungere un nuovo nome attività:

collection("Tasks").addDocument
    (data: ["name": textFieldReminder.text ?? 
        "empty task", "done": false])

Completa la parte finale dell'app inserendo quanto segue:

@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)
        
    }

Crea ed esegui l'app ancora una volta e, quando viene visualizzato il simulatore, prova ad aggiungere alcune attività, contrassegnarne alcune come completate e infine testare la funzione di eliminazione rimuovendo alcune attività. Puoi confermare che i dati memorizzati sono stati aggiornati in tempo reale passando alla console del database Firebase e osservando la raccolta e i documenti.

Filtraggio e query composte

Finora, hai lavorato solo con una semplice query, senza alcuna capacità di filtro specifica. Per creare query leggermente più solide, puoi filtrare in base a valori specifici utilizzando un whereField clausola:

docRef.whereField(“name”, isEqualTo: searchString)

Puoi ordinare e limitare i dati della tua query utilizzando order(by: ) e limit(to: ) metodi come segue:

docRef.order(by: "name").limit(5)

Nell'app FirebaseDo hai già utilizzato limit con la query di base. Nello snippet sopra, hai anche utilizzato un'altra funzione, le query composte, in cui sia l'ordine che il limite sono concatenati. Puoi concatenare tutte le query che desideri, come nell'esempio seguente:

docRef
    .whereField(“name”, isEqualTo: searchString)
	.whereField(“done”, isEqualTo: false)
	.order(by: "name")
	.limit(5)