Entity Framework
Entity Framework 6 è uno strumento molto potente che integrandosi perfettamente all'interno di progetti .NET consente di gestire interamente l'accesso al database in maniera efficente, intelligente e quasi trasparente al programmatore. Esso è pertanto un framework di persistenza e in quanto tale implementa nativamente svariati pattern architetturali che fra poco analizzeremo.
LINQ
Entity Framework non lavora da solo, bensì integra il suo funzionamento con un'altra componente molto importante di casa Microsoft che prende il nome di LINQ e mette a disposizione importantissimi metodi per l'interrogazione e la gestione delle Collection (array, liste, mappe, insiemi, ...).
Dalla fusione tra la potenza di LINQ e quella di Entity Framework nasce LINQ to SQL, una tecnologia che permette di utilizzare dei Metodi (tipici di un linguaggio orientato agli oggetti come C#) per effettuare delle Query SQL, che vengono prodotte in maniera del tutto trasparente. Ciò rende possibile l'interrogazione di un database gestito da Entity Framework tramite comode funzioni LINQ, in maniera del tutto indipendente dal DBSM.
LINQ to SQL implementa di fatto un Query Object Pattern, che si concretizza nell'interfaccia generica IQueryable
.
Riportiamo qui sotto un esempio che si occupa di eseguire una WHERE
sulla tabella Stocks
e successivamente una SELECT SUM(Quantity)
var ingredientStocks = db.Stocks.Where(x => x.IngredientId == element.IngredientId &&
x.ExpireDate >= DateTime.Now &&
x.Quantity > 0); // lista scorte (disponibili) per questo ingrediente
int? ownedQuantity = ingredientStocks.Select(x => x.Quantity)
.DefaultIfEmpty(0).Sum(); // quantità di questo ingrediente presente in magazzino
Entity Framework Pattern
Come già accennato, EF è un framework di persistenza che sfrutta numerosi pattern architetturali. Non tutti sono usati da Entity Framework in maniera classica, spesso sono mixati tra loro e alcuni di essi sono implementabili tramite specifici comportamenti da parte del programmatore. Provvediamo quindi a fornire un elenco completo di tutti i pattern che EF utilizza, simula o con cui è possibile utilizzarlo, per poi andare a descriverli singolarmente.
Rappresentazione e mapping dei dati
Alla base del funzionamento di Entity Framework vi è il pattern architetturale di dominio Table Module, su cui poi si appoggiano e lavorano gli altri pattern. Esso consiste nella rappresentazione di ciascuna tabella del database tramite una classe software. Ogni classe (che in EF prende il nome di Entità) è quindi rappresentazione di una tabella, mentre ogni proprietà di tale classe corrisponde ad una colonna della tabella. Ogni istanza della classe rappresenta una riga della relativa tabella nel db.
L'effettivo collegamento logico che sussiste tra classi e database è effettuato automaticamente da Entity Framework a patto che il programmatore segua alcune Naming Convention (sulle quali non ci dilungeremo). É comunque possibile sovrascrivere il comportamento di default e ridefinire il modo in cui le classi vengono mappate col database, implementando personalmente un Data Mapper Pattern che è altrimenti utilizzato in maniera trasparente dal framework. Tale possibilità permette di costruire anche una struttura gerarchica delle classi e mapparla adeguatamente con il database, sfruttando un pattern di tipo Inheritance Mappers.
Un'altra caratteristica importante di Entity Framework è la rappresentazione dell'intero database per mezzo della classe DBContext
, che contiene al suo interno tanti DbSet
quante sono le tabelle del database. Ogni DbSet
estende l'interfaccia Collection
ed è pertanto una Collection a tutti gli effetti, interrogabile da LINQ. Ciò corrisponde all'implementazione del patter Record Set, fondamentale ma ancora una volta trasparente al programmatore, che si limita ad utilizzare i DbSet
per le proprie interrogazioni.
Proprietà tipiche di un modello relazionale
Siamo sicuramente abituati ad immaginare la tabella di un database come un insieme di colonne, fra cui compare una Primary Key ed eventualmente delle Foreing Key. Normalmente le classi e gli oggetti di un linguaggio object oriented non necessitano di alcuna chiave che li identifichi; dovendo tuttavia mappare ogni classe del software con la tabella di un database, sarà necessario anche memorizzare le chiavi all'interno delle classi. Ciò consiste nell'implementazione di due pattern, Identity Field e Foreign Key Mapping che si occupano rispettivamente delle chiavi primarie e delle chiavi esterne, ancora una volta gestite automaticamente da Entity Framework.
Inoltre, nel caso in cui due classi siano collegate fra loro con un'associazione N:N è necessario introdurre una nuova Classe Associativa che rappresenti la tabella delle relazioni N:N presente anche nel database. In questo caso è necessario che il programmatore sia più consapevole di ciò che avviene e costruisca personalmente la nuova classe, secondo il pattern Association Table Mapping.
Lazy Load
Con il termine Lazy Load ci si riferisce a un pattern che è fondamentale in Entity Framework e di cui BrewDay fa largamente uso. Ciò consiste nel caricamento in memoria di alcune proprietà in modalità pigra, ossia solo nel momento esatto in cui queste vengono richieste a runtime.
Dato un oggetto che rappresenta una singola riga del database, il Lazy Load è particolarmente utile per riuscire ad accedere agli oggetti cui il primo è collegato tramite Foreign Key. Esemplifichiamo sulla classe Stock
, che rappresenta la disponibilità di un certo Ingrediente nel magazzino del Birraio:
public int StockId { get; set; }
public int IngredientId { get; set; }
public int Quantity { get; set; }
public virtual Ingredient Ingredient { get; set; }
É facile comprendere che tale classe abbia una propria chiave primaria StockId
, una chiave esterna IngredientId
verso la tabella Ingredient, a cui lo Stock fa riferimento, e una certa Quantity
. Ciò è il minimo indispensabile per rappresentare una riga della tabella Stock contenuta nel database.
A queste tre proprietà, aggiungiamo però una quarta proprietà di nome Ingredient
, tipo Ingredient
e marchiata come virtual
. Questa è una proprietà di navigazione e indica a Entity Framework che tale oggetto verrà caricato in maniera pigra solo se e quando un'istruzione ne farà richiesta (es: var x = db.Stocks.Find(id).Ingredient;
)
Come verrà inizializzato tale oggetto? É molto semplice seppur decisamente complicato sotto la scatola.
Entity Framework si incaricherà l'onere di fare per noi una SQL Join
sulla tabella Ingredient, cercando la riga che ha come chiave primaria IngredientId l'esatto valore contenuto nella proprietà IngredientId
dell'oggetto iniziale di tipo Stock
.
Le proprietà di navigazione sono in grado di risolvere automaticamente istruzioni di join per relazioni 1:1, 1:N e persino N:N.
Altri pattern utili
Con EF è possibile utilizzare dei pattern che non sono nativi del framework ma ben si integrano con esso. Abbiamo voluto citarne alcuni nonostante BrewDay non li utilizzi, perché molto ricorrenti in contesti simili al nostro.
Uno di questi è Unit of Work, utilizzato per effettuare operazioni atomiche, cioè un insieme di istruzioni per cui deve valere che se una di queste fallisce allora anche tutte le istruzioni precedenti devono essere annullate (rollback). Per chi ha un minimo di domestichezza con i database, avrà immediatamente capito che ciò è il corrispettivo delle transazioni in SQL, implementabili grazie a questo pattern anche lato software.
Nonostante il pattern NON sia particolarmente intuitivo, è possibile simularlo tramite il classico Entity Framework.
Ho precedentemente citato la classe DbContext
, che rappresenta di fatto il database. EF prevde che tutte le modifiche effettuate sul contesto non vengano propagate direttamente al database, ma solamente quanto il programmatore chiama il metodo SaveChanges()
. É possibile sfruttare questa semplice proprietà per effettuare modifiche sequenziali sul contesto ma "annullarle" tutte nel caso in cui si verifichi un errore prima che si arrivi alla SaveChanges()
; di fatto le modifiche non vengono annullate, ma semplicemente non propagate al database e quindi di fatto perse, dando come risultato un'artigianale transazione. BrewDay sfrutta questo meccanismo nell'algoritmo che implementa l'avvio di una Produzione e il What Should I Brew Today.
Altri due pattern spesso utilizzati sono Repository e Service Layer, che frappongono uno strato intermedio fra Controller e DbContext per una più consapevole gestione dell'accesso al database e delle regole di business. Non ci dilungheremo oltre nella spiegazione di questi due pattern.
Code First
Entity Framework è uno strumento talmente potente che è in grado di fare reverse engineering in ben tre direzioni:
- generare il codice a partire da un database
- generare il codice e il database a partire da un modello
- generare il database a partire dal codice
Il terzo approccio prende il nome di Code First ed è stato quello da noi scelto per lo sviluppo del progetto. Questo ci ha permesso di slegare completamente la nostra attenzione dal database effettivo e concentrarci solamente sulle classi software. In questo proceso è stato particolarmente d'aiuto Visual Studio 2017, che ha automatizzato la comunizazione fra EF e il DBMS integrato SQL Server 2016 Express LocalDB.
Ciò ha portato di fatto alla realizzazione del solo diagramma delle classi, contentente le Entità utilizzate da Entity Framework per la creazione e aggiornamento del database secondo un sistema di migrazioni automatiche.