Primi passi con EF Code First ( parte 2)

Scritto da  Antonio Pierascenzi il mercoledì 4 aprile 2012
Linguaggio: C#   •  Framework: 3.5,4.0   •  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:

  1. Classe A : proprietà list<classe B> à  classe B: proprietà riferimento classe A  = relazione uno a molti
  2. Classe A : proprietà semplice o proprietà list<classe B>  = relazione uno a molti
  3. Classe A : proprietà list<classe B> à  classe B proprietà list<classe A> = relazione molti a molti
  4. 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:

EFCF-2parte-1

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:

EFCF-2parte-2

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:

EFCF-2parte-3

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:

EFCF-2parte-4

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à è:

EFCF-2parte-5

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:

EFCF-2parte-6

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:

EFCF-2parte-7

EFCF-2parte-8

Aggiungendo un'azienda

EFCF-2parte-9

Otterremo nel database:

EFCF-2parte-10

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:

EFCF-2parte-11

Se volessimo inserire un Mvp nel nostro archivio scrivendo :

EFCF-2parte-12

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.

EFCF-2parte-13

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:

EFCF-2parte-14

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.

EFCF-2parte-15

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.

EFCF-2parte-16

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

EFCF-2parte-17

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:

EFCF-2parte-18

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

EFCF-2parte-19

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

 
x