Primi passi con EF Code First ( parte 2)
Scritto da
Antonio Pierascenzi
il
mercoledì 4 aprile 2012
Linguaggio:
•
Framework:
•
Livello: 200
Download pdf
Primi passi con Entity
Framework Code First (parte 2)
Nel precedente articolo abbiamo visto come operare sulle nostre
classi per ottenere delle configurazioni personalizzate per
modificare le convenzioni sui cui agisce CF in fase di parsing
delle nostre classi di dominio. Abbiamo visto come CF operi sulla
base di convenzioni e ci siamo soffermati su alcune di quelle
definite "di base" per la creazione dello strato di persistenza dei
nostri dati. Abbiamo anche visto come sia possibile operare sia a
livello di codice tramite annotazioni nelle nostre classi oppure
tramite l'uso delle Fluent Api che ci permettono di scrivere codice
personalizzato per il nostro contesto sia all'interno della classe
di context sia in una classe esterna opportunamente definita e
caricata in fase di startup del progetto. In questo articolo ci
occuperemo di come CF gestisce l'ereditarietà tra le classi del
nostro dominio e come sia possibile definire delle strategie custom
di relazioni tra classi per ottenere un mapping specifico tra le
tabelle del database che verrà creato dall'engine di CF. Il
collegamento può essere unidirezionale, definizione di una
proprietà collegata solo su una classe, e bidirezionale,
definizione di proprietà su entrambe le classi da collegare.
Definizioni di chiavi esterne
Abbiamo visto come CF applichi regole specifiche per gestire le
associazioni tra le classi se sono presenti o meno le cosiddette
Navigation Property, riferimenti semplici o liste verso la classe
che si vuole collegare. A seconda di come definiamo queste
proprietà è possibile definire il tipo specifico di collegamento
che si vuole ottenere operando come al solito sia attraverso
annotazioni sulle classi oppure tramite le Fluent Api.
Le regole di base su cui opera CF sono schematizzate di
seguito:
- Classe A : proprietà list<classe B> à classe B:
proprietà riferimento classe A = relazione uno a
molti
- Classe A : proprietà semplice o proprietà list<classe B>
= relazione uno a molti
- Classe A : proprietà list<classe B> à classe B
proprietà list<classe A> = relazione molti a
molti
- Classe A : proprietà semplice classe B à classe B
proprietà semplice classe A> = relazione uno a uno
Nell'ultimo caso però dobbiamo specificare noi quale sia la
dipendenza tra le due classi, perché nei precedenti casi CF
"inferisce" che la classe contenente un riferimento all'altra
è dipendente e, nel database che verrà creato, la corrispondente
tabella conterrà una chiave esterna; nelle classi contenenti liste
(relazione molti a molti) il collegamento sarà impostato in
un'ulteriore tabella tramite due chiavi esterne. Per
impostare la dipendenza per il caso 4 dobbiamo aggiungere la
definizione delle chiavi per le proprietà delle nostre classi
reputate ad esserlo; abbiamo visto nel precedente articolo come e
cosa dobbiamo fare per definire una chiave, ora vediamo come fare
per definire una chiave esterna via data annotation, ad esempio
sulla classe SpeakerPhoto del nostro progetto:

Questa annotazione personalizzata ci permette di definire la
classe SpeakerPhoto come "dipendente" dalla classe Speaker;
nell'esempio che segue vediamo cosa scrivere via codice fluent:

Dove con HasRequired e WithOptional indichiamo
al motore di CF che la classe Speaker può esistere senza una
relativa proprietà SpeakerPhoto mentre la classe SpeakerPhoto deve
avere una proprietà Speaker.
Quando si definisce una chiave esterna CF, per determinare
se la relazione tra le classe sia richiesta o opzionale, controlla
se la proprietà impostata sia nullable e, come abbiamo visto con
l'esempio precedente, questa impostazione si può modificare via
codice fluent o via data annotation aggiungendo l'attributo
Required sulla navigation property desiderata.
Ciò comporta, nella tabella che verrà creata, l'impostazione
della colonna relativa con valore not null. Per completare
il discorso sulla definizione del tipo di relazione tra le
classi, dobbiamo dire che è possibile definire la cosiddetta
molteplicità attraverso l'uso di tre opzioni, di cui una vista
in precedenza :
- Optional (una proprietà può avere una sola istanza o
essere nulla)
- Required (una proprietà deve avere un'istanza)
- Many (una proprietà con una lista di single
istanze).
Attraverso I metodi:
- HasOptional
- HasRequired
- HasMany
E attraverso l'uso, nei casi in cui sia necessario, di ulteriori
impostazioni sulle classi "proprietarie" della relazione:
- WithOptional
- WithRequired
- WithMany
Questo approccio ovviamente va usato nei casi in cui abbiamo la
necessità di dover definire una chiave esterna laddove la
definizione delle nostre classi non trova corrispondenza con le
convenzioni sui cui si poggia CF riguardo la costruzione di chiavi,
nel nostro caso, esterne.
Ci sono scenari in cui abbiamo bisogno di un occorrenza
multipla di uno stesso riferimento ad una classe verso una classe
esterna, CF in questi casi non sa come agire se non, secondo le sue
convenzioni, creare nella classe esterna due identificativi
identici e quindi nella relativa tabella due chiavi esterne,
afferente, però, nel nostro intento, alla stessa entità collegata.
Questo scenario ci fornisce l'occasione di introdurre le cosiddette
Inverse Navigation Properties che come sempre in questi casi, è
possibile schematizzarne l'uso attraverso un esempio.
Riprendendo la nostra soluzione, potremmo voler tracciare nella
classe Speaker un secondo dato riguardante un ulteriore
Conference (ad esempio un lab) aggiungendo una proprietà
LabConference di tipo Conference nella classe Speaker e
un'ulteriore navigation property lista di Speaker per i lab:

Per convenzione, come abbiamo detto, CF assume che ci sia
bisogno di un ulteriore relazione tra le due tabelle e, se
eseguissimo questo codice, creerebbe un'altra chiave esterna per la
relazione che trova in fase di parsing delle entità, proprio perché
non sa quelle delle due navigation property considerare quella
reputata per la creazione del riferimento su cui agganciare la
chiave esterna. Per istruire CF su quale sia questa proprietà è
necessario l'utilizzo di una keyword apposita, nel caso in cui
volessimo operare tramite annotazione:

InverseProperty, che necessita come parametro del nome
della corrispondente proprietà nella classe referenziata.
Via Fluent Api viceversa il codice da scrivere nella classe di
configurazione o in quella di context nel metodo
OnModelCreating relativo ad entrambe le proprietà è:

Nell'ambito della definizione delle nostre classi di dominio, ci
potremmo trovare nelle situazioni in cui abbiamo la necessità di
definire delle relazioni molti a molti fra le entità. Questo tipo
di relazioni vengono regolarmente considerate da CF attraverso le
consuete convenzioni, che, nello specifico, interpretano la
presenza di due liste di oggetti diametralmente opposte nelle
entità come la volontà di eseguire il mapping multiplo da noi
desiderato.
Nella nostra soluzione potremmo aggiungere ad esempio una
classe, Languages, contenente una lista di Speakers e un ID,
e nella classe Speakers aggiungere una lista di Languages; il
risultato, senza aggiungere nulla dal punto di vista della
configurazione, sarà quello di avere una tabella associativa in più
che prenderà il nome, secondo il pattern
NomePrimaEntitàNomeSecondaEntità, di SpeakerLanguages
contenente due chiavi esterne dal nome e valore delle due chiavi
primarie delle relative entità associate dalle due navigation
property.
Convenzioni per il database
Nell'articolo precedente abbiamo visto come configurare in
maniera personalizzata il nome e lo schema da dare alle nostre
tabelle nel caso in cui non ci piaccia come vengano definite
da CF (ricordiamo che il servizio di Pluralization di CF si basa
sulle parole inglesi) sia con annotazioni che con codice fluent, e
abbiamo visto come sia possibile fare la stessa cosa per le
proprietà corrispondenti alle colonne sia per quanto riguarda il
nome che il tipo e la lunghezza; di seguito tratteremo come mappare
a nostro piacimento più classi in unica tabella o una singola
classe su più tabelle e come CF si comporta riguardo gli scenari di
ereditarietà fra classi.
Il primo scenario è stato in parte coperto indirettamente quando
facevamo riferimento alla definizione delle chiavi esterne per quei
casi in cui serve una relazione uno a uno; più in generale possiamo
dire che CF mappa le proprietà di un entità su una tabella
comune nei casi in cui siano soddisfatte le regole per cui le
entità devono avere una relazione uno a uno ed esista una chiave
comune. Nel nostro esempio le entità Speaker e SpeakerPhoto
soddisfano questi requisiti (controlliamo che la proprietà
SpeakerPhoto nella classe Speaker sia configurata come Required).
Questo però non basta, poiché se eseguissimo tale configurazione ci
accorgeremmo che CF crea due tabelle, Speakers e SpeakerPhoto, che
ovviamente non rispondono ai nostri requisiti. Per ovviare a questo
dobbiamo agire tramite l'annotazione sulle tabelle:

Abbiamo già visto nel precedente articolo come fare la stessa
cosa via fluent Api attraverso l'uso della keyword
ToTable.
Questo tipo d mapping è comunemente definito Table
Splitting.
Se invece avessimo bisogno di configurare le nostre classi e
relative tabelle in uno scenario esattamente opposto al precedente,
ossia più tabelle per singola entità, per esempio se nel nostro
caso volessimo aggiungere una classe per gestire le aziende che
vorranno essere sponsor dell'evento, potremmo voler separare, per
svariati motivi, il dettaglio relativo ai banner rispetto alle
altre informazioni.
Questo mapping è definito entity splitting e
purtroppo non è possibile configurarlo tramite annotazioni, le
quali non hanno il concetto di subset di proprietà, ma si deve
usare via fluent Api il metodo Map del quale vediamo un
esempio:
la classe Company e relativa configurazione:


Aggiungendo un'azienda

Otterremo nel database:

Come vediamo CF si occupa di creare la chiave esterna che
garantisce la relazione con la tabella da cui parte la dipendenza
rispetto alla nostra proprietà Banner e alla relativa tabella.
Convenzioni per l'ereditarietà
CF supporta diversi tipi di ereditarietà, di default applica
l'eredità per gerarchia (TPH) tramite la quale viene usata
una tabella contenente tutte i dati delle varie classi e viene
aggiunta una colonna che discrimina i tipi (Discriminator)per ogni
riga della tabella il cui valore è rappresentato dal typename della
classe. E'possibile personalizzare il nome della colonna
discriminante attraverso il metodo Map; per fare un
esempio modifichiamo la classe Speaker del nostro progetto
commentando la proprietà IsMvp e aggiungendo una classe Mvp che
eredita da Speaker dove aggiungiamo altri dettagli in riferimento
all'appartenenza dello speaker al gruppo degli MVP:

Se volessimo inserire un Mvp nel nostro archivio scrivendo :

Eseguendo il progetto potremmo notare che, senza aggiungere
nulla nella parte di configurazione, CF inserirà in un'unica
tabella tutte le proprietà della classe Speaker e le sua derivata
MVP aggiungendo una colonna Discriminator il cui valore assumerà, a
seconda di quello che inseriremo, "Mvp" oppure "Speaker", il tipo
sarà nvarchar(128) e not null.

Il nostro scopo però potrebbe essere quello di voler modificare
qualcosa in questo meccanismo, vediamo come fare per configurare
adeguatamente il nostro progetto, sapendo che è possibile solo
utilizzando del codice Api come già notato nel caso
dell'entity splitting visto in precedenza.Infatti, se volessimo
modificare il tipo e il nome della colonna Discriminator, dovremmo
usare ancora una volta il metodo Map in questo modo:

Il metodo Requires ci permette di specificare di voler inserire
una Colonna Discriminator e il metodo HasValue il suo
valore specifico in riferimento al tipo valutato dall'engine.
Un altro modo che CF possiede per gestire l'ereditarietà,
Table Per Type (TPT), consiste nel fatto di disporre i
singoli tipi della gerarchia di classi che ereditano dal tipo base
in singole tabelle. Le proprietà delle classi derivate vengono
salvate nelle relative tabelle collegate alla tabella del tipo base
tramite una chiave esterna mentre l'identity key è l'id della
classe base. La configurazione personalizzata è molto semplice ed è
possibile farla seguendo le stesse regole utilizzate, sia con data
annotation che Fluent Api, in sede di dichiarazione del nome della
tabella da dare al tipo.

L'ultima modo di gestire l'ereditarietà da parte di CF è
rappresentato dal cosiddetto Table Per Concrete Type
(TPC), che è molto simile al TPT con la differenza che tutte
le proprietà del tipo comprese quelle ereditate vengono salvate
nella tabella relativa che non avrà più legami con la tabella del
tipo base attraverso chiavi esterne o ereditate. Anche per questo
tipo di mapping non esiste la possibilità di utilizzare le data
annotation per configurarlo, riprendendo l'esempio precedente
potremmo configurare in questo modo la classe Speaker e Mvp.

Dove, attraverso l'uso del metodo
MapInheritedProperties accessibile solo tramite l'uso di
Map , diciamo a CF di voler mappare tutte le proprietà
delle classi derivate da una classe base nelle rispettive colonne
di una nuova tabella denominata Mvp.
Uso delle classi astratte nel modello dati
Finora abbiamo trattato, nel modello di esempio, classi normali
di cui abbiamo parlato in parte qualche dettaglio riguardo
l'ereditarietà; ma spesso nei nostri progetti abbiamo a che fare
con classi astratte da cui partire per implementare classi di uso
reale nel nostro dominio. Se ad esempio modificassimo la nostra
classe Speaker rendendola astratta, ovviamente non potremmo più
utilizzarla direttamente creandone un'istanza ma dovremo creare
delle classi derivate, come abbiamo fatto con la classe MVP.
Aggiungiamo per esempio la classe Partner(perdonate la
scarsa fantasia):

Rimuovendo le configurazioni fatte in precedenza ed inserendo un
Mvp e un Partner, noteremo che CF tratterà questo caso come se la
classe base non fosse astratta utilizzando la convenzione di
default per il mapping dell'ereditarietà, la TPH:

Che nel database si riflette ovviamente in un'unica tabella con
tutte le proprietà delle classi derivate e la colonna
discriminatore:

Il metodo GetAllSpeakers l'abbiamo aggiunto per far vedere,
(potete farlo in debug nel punto in cui viene popolata la lista),
la query Sql che genera CF dove potrete osservare che vengono prese
in considerazione con un Select In solo le classi derivate dalla
classe astratta:
{SELECT
[Extent1].[SpeakerId] AS [SpeakerId],
[Extent1].[Discriminator] AS [Discriminator],
[Extent1].[Name] AS [Name],
[Extent1].[Surname] AS [Surname],
[Extent1].[Category] AS [Category],
[Extent1].[Year] AS [Year],
[Extent1].[Company] AS [Company],
[Extent1].[Benefit] AS [Benefit],
[Extent1].[Conference_ConferenceId] AS [Conference_ConferenceId]
FROM [dbo].[Speakers] AS [Extent1]
WHERE [Extent1].[Discriminator] IN ('Mvp','Partner')}
per evitare di includere altri tipi che non sono parte del
modello dati ma che sono stati salvati nella stessa tabella.
Anche per questi scenari è possibile modificare il tipo di
mapping in TPT o TPC nei modi visti in precedenza ad esempio per
ottenere i nomi delle tabelle ognuno per tipo derivato, in questo
caso non serve specificare il nome della tabella della classe base
che di default viene presa da CF come il typename della classe
stessa.
Con questo abbiamo terminato questa miniserie di articoli sulle
configurazioni custom che è possibile applicare per istruire CF a
nostro piacimento, ovviamente l'argomento è lungi da essere
terminato ma, come sempre, lo scopo è quello di stuzzicare il
vostro "appetito" e provare ad usare CodeFirst nei vostri progetti
sapendo dove mettere le mani in caso di bisogno. In un prossimo
articolo sul portale della community tratteremo l'argomento
riguardante il controllo della creazione/modifica e deploy del
database nei vari ambienti del nostro progetto. Il riferimento come
sempre è il blog del team di Ado.Net
Tags: EF4,CodeFirst,DataAnnotation,FluentApi