Nonostante gli ORM esistano da almeno un paio di decenni ormai, è ancora aperta la diatriba se utilizzarli oppure no: Entity Framework o NHibernate?
Oppure usare Dapper? O ancora direttamente query ad hoc con ADO.NET? Stored procedures.
I due modi di vedere la cosa nascono dal fatto che il DBA (Data Base Administrator, il progettista della base dati) è stata figura predominante agli albori dell'informatica e solo successivamente si è affermata quella dello sviluppatore; quest'ultimo spesso con il codice SQL non si è mai trovato granché a suo agio.
Ecco perché il DBA in certi casi è diventato sviluppatore, occupandosi del linguaggio SQL, sentendosi più a suo agio rispetto all'utilizzo di codice applicativo come C#, C++ o altri linguaggi più consoni a descrivere la logica applicativa.
Considera che le applicazioni sono mediamente costituite per il 90% da codice ad oggetti (OOP), e solo la restante parte è dedicata all'accesso al database.
Se consideriamo tutte le applicazioni line of business, cioè quelle che sono il core business delle aziende, principalmente troviamo applicazioni che fanno proprio accesso ed elaborazione dei dati nel modo più veloce possibile, che è diventata la sfida principale dell’informatica insieme alla capacità di aggiungere costantemente nuove funzionalità in tempi molto rapidi.
La necessità è di avere applicazioni altamente performanti e allo stesso tempo capaci di evolvere velocemente per rispettare le nuove esigenze del mercato; di fatto sono le richieste di nuove funzionalità da parte degli utenti e miglioramento di quelle attuali all'interno del software.
La nostra sfida come progettisti di software è quella di creare applicazioni estremamente veloci che crescano rapidamente e che rispettino i requisiti degli utenti e non quelli degli sviluppatori. Infatti, gli sviluppatori tendono spesso a sovra ingegnerizzare le applicazioni utilizzando soluzioni che non servono o che non offrono grande valore aggiunto in quel momento.
Sviluppare in previsione di successive necessità è un errore grave che fanno molti sviluppatori e molti team, perché non rispetta il principio KISS, cioè Keep It Simple and Stupid, tieni le cose semplici e stupide, dove per stupide si intende che siano molto facili da capire e modificare, evitando pezzi di codice che sono un groviglio di condizioni, if, classi monolitiche e metodi chilometrici.
Meno codice scrivi migliore è il sistema
Perché l’architettura e il codice siano i migliori possibili, devono fare il minimo indispensabile.
Less is better than more, pur ovviamente rispettando i requisiti funzionali, cioè l’applicazione deve fare quello per cui è stata progettata, ma con il minimo sforzo.
Minima spesa con massima resa, il nostro lavoro è scrivere meno codice possibile per ottenere il risultato.
Pattern particolarmente complicati, soluzioni difficili da manutenere, soluzioni con un sacco di righe di codice per poter essere implementate, soluzioni che dipendono da configurazioni complesse, o la scrittura di troppo codice di infrastruttura per riuscire ad aggiungere una nuova funzionalità ad aggiungere un nuovo end point, sono tutti gli acerrimi nemici dello sviluppatore e del team.
Verrebbe quindi naturale pensare che tra le due soluzioni, cioè tra utilizzare ADO.NET per accedere ai dati con query dirette, rispetto all'utilizzo di un qualsiasi ORM, quest'ultima sia la soluzione migliore per rispettare quello che ho appena detto sopra: sviluppare una applicazione semplice e facile da mantenere.
Ora il punto è proprio questo, il codice SQL iniettato direttamente nel codice .NET non è affatto un codice facile da scrivere e manutenere.
Infatti, all'interno di quel codice non è possibile applicare le funzionalità di intellisense di Visual Studio che ormai ci permettono di scrivere in automatico gran parte del codice e probabilmente uno dei motivi maggiori di successo proprio di utilizzo di un IDE come Visual Studio o Visual Studio Code.
Sulla base di questo ragionamento scelgo Entity Framework rispetto ad altri ORM, perché è la soluzione ufficiale di Microsoft e quindi sviluppata, supportata e mantenuta da Microsoft.
Rispetto ad altri framework che invece seppur siano soluzioni interessanti e assolutamente apprezzabili perché implementate dalla community, hanno il rischio di essere poi abbandonate oppure di non risolvere velocemente le segnalazioni degli sviluppatori, proprio perché non hanno dietro un'azienda in grado di offrire un servizio continuativo.
Entity Framework offre sicuramente dei vantaggi rispetto all'utilizzo di ADO.NET diretto perché è possibile creare il mapping del database attraverso delle classi che rappresentano il nostro dominio applicativo e che ci consentono di scrivere quindi molto più velocemente sfruttando la potenza dei LINQ.
P.S.
Si pronuncia “linc”, non “lincu”, come sento dire da tutti!
La potenza di LINQ viene dal fatto che una volta che noi abbiamo descritto il nostro dominio applicativo con le nostre classi che rappresentano le tabelle del DB, che vengono modellate anche in automatico
Modello relazionale contro il modello a oggetti
Il modello relazionale è poco rappresentativo della realtà, perché ragiona solamente a rettangoli attraverso le tabelle e non in modo gerarchico e insiemistico, come invece consente di fare la programmazione OOP.
Il codice ad oggetti ci consente di rappresentare il mondo reale attraverso le classi, relazioni di ereditarietà, composizione e il polimorfismo, principi fondamentali che - se usati nel modo corretto - consentono di scrivere codice molto più pulito.
Questo trasferimento di rappresentazione di modello ci consente di scrivere codice più rapidamente, perché riusciamo a ragionare con degli oggetti più articolati, scrivendo dei metodi che hanno della logica all'interno del set delle proprietà.
Ora dobbiamo fare un distinguo molto importante nel senso che l’ORM è sicuramente molto utile in fase di lettura dei dati ma diventa più complicato in fase di scrittura perché salvare un grafo di un oggetto complesso richiede un sacco di codice di mapping.
Che, naturalmente viene scritto (o generato), una volta sola, però va scritto per ogni tabella per ogni nuova funzionalità che vogliamo scrivere e di cui non abbiamo ancora le strutture dati.
È necessario fare un sacco di check del codice che abbiamo scritto: infatti, persistere un grafo può coinvolgere più tabelle e richiedere di verificare oggetti che contengono relazioni con altri oggetti, relazioni con padri e tabelle che rappresentano una gerarchia.
Una bella ragnatela di tabelle e relazioni insomma.
Questo è proprio un esempio di design sbagliato, con cui erano “progettati” i vecchi gestionali e con cui molti gestionali in Italia vengono ancora implementati: una marea di campi che trasformano un form in un albero di Natale, che il povero utente è costretto a compilare.
Spesso si trova una logica di validazione molto stretta che gli impone di inserire tutti i dati prima di potersi muovere all'interno dell'applicazione e quindi poter effettuare il salvataggio.
Lo obbliga a perdere molto tempo a compilare tutta la vista, piuttosto che avere la possibilità come offrono le applicazioni moderne, come quelle sviluppate negli Stati Uniti negli ultimi 15 anni da cui però gli italiani non hanno capito e non hanno imparato un granché visto che si ostinano a fare applicazioni monolitiche, con una marea di campi.
Questa possibilità di separare le diverse proprietà in piccoli comandi ci consente chiaramente di avere del codice molto più semplice e snello.
Per risolvere questo problema ci sono fondamentalmente due strade:
- Un solo database: utilizzando un ORM per leggere i dati e ADO.NET per scriverli
- Storage separati: con CQRS dividere lo storage di lettura da quello di scrittura
La soluzione con un solo database
Il primo approccio è quello classico, basato sull’utilizzo di un solo database, ma ottimizzando performance di lettura e scrittura creando micro comandi e micro letture.
I micro comandi consentono di fare delle piccole variazioni sull’aggregate principale usando il “trucco” di lavorare al massimo su 4-5 proprietà contemporaneamente coinvolte nella operazione di business, invece che salvare tutto un grafo complesso in una volta sola.
La modifica che facciamo sull’aggregate non è una modifica che coinvolge decine e decine di proprietà e soprattutto non coinvolge il grafo intero, ma tipicamente solo una piccola porzione dell'oggetto principale o uno dei suoi figli.
Ti faccio un esempio semplice: se ho un customer che ha una serie di indirizzi, non avrò la necessità di dover mostrare e salvare tutto il customer, incluse altre informazioni come la partita IVA, il fatturato e altre informazioni relative ai clienti.
Ma poi ho la possibilità di salvare singolarmente ogni campo o una serie di campi collegati, come ad esempio appunto l'aggiunta di indirizzi categorizzati per tipo.
Per cui il codice di salvataggio sarà estremamente semplice e anche estremamente veloce: perché i lock avrò sul database saranno molto più piccoli, con un numero di tabelle e colonne coinvolte molto limitato, con un gran vantaggio per le performance.
Questo ti spiega perché l'utilizzo di un ORM per salvare i dati non è la scelta più performante, proprio perché abbiamo la necessità di travasare i dati da un oggetto ad delle tabelle.
La soluzione con database separati e CQRS
Secondo il pattern CQRS (Command Query Responsibility Segregation) di norma viene separata completamente la parte di lettura dei dati da quella di scrittura, sia a livello di macchine che ospitano le API anche attraverso un'architettura a micro-servizi, sia a livello di storage, dove potremmo avere un DB relazionale per la lettura dei dati e un DB documentale come CosmosDB, per la scrittura dei dati.
Se usiamo un database documentale come Cosmos DB possiamo ragionare a documenti. Il documento è un'entità auto consistente che vive di vita propria.
Possiamo immaginarlo come fosse un documento di Word o un’immagine di Photoshop, in cui tutte le informazioni di cui abbiamo bisogno sono all'interno del documento stesso.
Ci possono essere delle relazioni con altri documenti, ma di base il grafo diventa un documento unico.
In questo caso il database documentale ci viene incontro perché a quel punto il dato verrà salvato serializzandolo tramite JSON e quindi non avremo la necessità di fare mapping dal dato al database alle tabelle del database relazionale, per cui la persistenza sarà molto più semplice e molto più veloce, perché alla fine si tratta di un salvataggio di un file.
Non avremo tutto il lavoro dovuto al mapping dei dati.
Quando usare Entity Framework
Veniamo ora all'utilizzo di Entity Framework per la parte di lettura. Nel caso della lettura dei dati di nuovo l'interfaccia utente deve rispecchiare una logica di business semplice, anche se complessa.
Nel senso che l’interfaccia deve mostrare una piccola quantità di dati. Sai bene anche tu che far vedere all’utente 100 righe in una tabella, nel 99% delle volte non serve a niente, perché non è in grado di leggerle e confrontarle contemporaneamente.
Il massimo che un essere umano mediamente dotato riesce a fare, è lavorare su 5 o 10 righe con un numero molto ristretto di colonne. Quindi per migliorare le performance bisogna necessariamente lavorare sul design della UI.
E d'obbligo quindi fare un lavoro di ottimizzazione riducendo la quantità di dati che si spostano da e verso i sistemi remoti dalle API.
In questo scenario abbiamo dei dati che sono piuttosto piccoli, che contengono poche proprietà e che hanno una bassa strutturazione del dato.
Pochi dati ma buoni
Una bassa strutturazione del dato significa che lato interfaccia utente avrò più cambi di vista per poter andare nel dettaglio e vedere il dato nella sua interezza.
Ad esempio, se ho una fattura non avrò più, come nei vecchi gestionali brutti e difficili da usare, un'unica maschera che mi mostra tutti i dati della fattura quindi i dati relativi al cliente e i suoi dati fiscali, le linee d'ordine, i dati di pagamento eccetera.
Ho invece una suddivisione in cui ognuna di questa porzione di dati si trova in una vista differente.
Sfruttando tab o un wizard ho l'intestazione in uno step, le righe d'ordine all'interno di un altro step, il riepilogo in un altro step, le informazioni relativi ai pagamenti in un'altra vista ancora.
A questo punto i dati che devo spostare per renderizzarli all'interno dell'interfaccia utente o mettendoli a disposizione di servizi esterni saranno oggetti con poche proprietà e bassa strutturazione.
Il vantaggio è quello di poter avere performance ottimali, perché la suddivisione di questo tipo ci permette di spostare poche centinaia di byte alla volta, potendo essere molto molto veloci e sfruttare al meglio le performance dei sistemi cloud che coinvolgono sempre l'utilizzo di Internet.
Al contrario di sistemi on premise che invece utilizzano la rete locale, proprio la necessità di ottimizzare sia il trasferimento dei dati tramite HTTP che delle query sul database ci consente di creare sistemi estremamente efficienti e anche estremamente scalabili.