Continuiamo il nostro percorso di studio della CTP 5 di EF, tenendo sempre presente quanto detto nel post precedente o meglio nei commenti. Vediamo come possiamo utilizzare Data Annotations e Fluent API per eseguire l’override delle convenzioni di default usate in code first per mappare la base di dati sottostante: impostare il nome delle tabelle e/o colonne, lunghezza dei campi, definizione delle chiavi, definizione delle relazioni ecc. La volta precedente, utilizzando Code First generavamo il database rappresentato dal diagramma seguente:
Partiamo dal database e apportiamo qualche modifica alle tabelle in questo modo: rinominiamo Projects in Progetto, Skills in Competenza, ProjectsDeveloper in ProgettoSviluppatore e aggiungiamo una nuova tabella Architetto contenente colonne che estendono le informazioni contenute da Sviluppatore.Rinominiamo anche le colonne in modo da avere anche uno schema di database in italiano:
Avendo apportato queste modifiche, se provassimo ad eseguire il codice scritto nel precedente post (utilizzando la strategia di inizializzazione del database custom), verrebbe sollevata un’eccezione System.Data.EntityCommandExecutionException , la quale indica che l’oggetto dbo.Developers non è più valida (ovviamente). Sfruttiamo gli attributi delle Data Annotations: aggiungiamo al nostro progetto un riferimento alla libreria System.ComponentModel.DataAnnotations.dll. Tramite attributi Table(…) istruiamo gli oggetti specificando a quali tabelle devono essere mappati:
[Table("Sviluppatore")]
public class Developer
{
...
}
[Table("Progetto")]
public class Project
{
...
}
[Table("Competenza")]
public class Skill
{
...
}
Se provassimo ad eseguire l’applicazione, avremmo ancora un'eccezione perché la tabella di associazione ProgettoSviluppatore (o meglio ProjectDeveloper) non è conosciuta. Il messaggio dell’eccezione sollevata sarebbe: Invalid object name 'ProjectDeveloper'. Avremmo comunque diversi problemi dovuti alle operazioni di rename delle colonne nelle tabelle fisiche del database. Dobbiamo “addrestrare” opportunatamente code first eseguendo l’override del metodo OnModelCreating nella classe derivata da DbContext (la classe Db).
protected override void OnModelCreating(System.Data.Entity.ModelConfiguration.ModelBuilder modelBuilder)
{
//Columns Rename.
modelBuilder.Entity<Developer>()
.Property(d => d.Name).HasColumnName("Nome");
modelBuilder.Entity<Developer>()
.Property(d => d.Surname).HasColumnName("Cognome");
modelBuilder.Entity<Skill>()
.Property(s => s.Description).HasColumnName("Descrizione");
modelBuilder.Entity<Project>()
.Property(p => p.Description).HasColumnName("Descrizione");
////Set many-to-many relationship.
modelBuilder.Entity<Developer>()
.HasMany<Project>(d => d.Projects)
.WithMany(p => p.Developers)
.Map(m =>
{
////Association table.
m.ToTable("ProgettoSviluppatore");
////Map left Key.
m.MapLeftKey(d => d.Id, "SviluppatoreId");
////Map Right Key.
m.MapRightKey(p => p.Id, "ProgettoId");
});
////Set relationship between developer and skill.
modelBuilder.Entity<Developer>()
.HasMany<Skill>(d => d.Skills)
.WithOptional()
.IsIndependent()
.Map(m => m.MapKey(d => d.Id, "SviluppatoreId"));
}
Modifichiamo il Main:
using (Db db = new Db())
{
try
{
int developers = 0;
int projects = 0;
////Add a new Developer
Developer dev1 = new Developer()
{
Name = "Pinco",
Surname = "Pallo",
Skills = new Skill[]{
new Skill (){ Description ="C# 4.0", Rank=4},
new Skill (){ Description="LINQ", Rank =4},
new Skill (){ Description="Entity Framework" ,Rank=4}
}
};
////Add a new Project
Project prj = new Project() { Description = "A new e-commerce web site." };
////Assign developers to project.
prj.Developers.Add(dev1);
dev1.Projects.Add(prj);
db.Developers.Add(dev1);
////Save data.
int rowsAffected = db.SaveChanges();
developers = (from d in db.Developers select d).Count();
projects = (from p in db.Projects select p).Count();
Console.WriteLine("Projects in db: {0}", developers);
Console.WriteLine("Developers in db: {0}", developers);
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
ed eseguiamo:
Avendo aggiunto la tabella Architetto, abbiamo necessità di aggiungere una nuova classe Architect:
[Table("Architetto")]
public class Architect : Developer
{
public string Email { get; set; }
public string Phone { get; set; }
}
modifichiamo il codice di OnModelCreating aggiungendo:
modelBuilder.Entity<Architect>()
.Property(a => a.Phone).HasColumnName("Telefono");
modifichiamo il Main per aggiungere un Architetto al progetto con competenze più alte dello sviluppatore ( ) :
//Add an architect.
Architect arch1 = new Architect()
{
Name = "Tizio",
Surname = "Caio",
Email = "tizio.caio@pm.it",
Phone = "06123",
Skills = new Skill[]{
new Skill (){ Description ="C# 4.0", Rank=5},
new Skill (){ Description="LINQ", Rank =5},
new Skill (){ Description="Entity Framework" ,Rank=5},
new Skill (){ Description="VB.NET 4.0" ,Rank=5},
new Skill (){ Description="Project Managament" ,Rank=5}
}
};
prj.Developers.Add(dev1);
dev1.Projects.Add(prj);
prj.Developers.Add(arch1);
arch1.Projects.Add(prj);
db.Developers.Add(dev1);
db.Developers.Add(arch1);
ed eseguiamo. Dovremmo ottenere quanto segue:
Si, ok, tutto bello, ma cosa abbiamo fatto ? Tramite Fluent API, diciamo:
1) Il Developer (mappata sulla tabella Sviluppatore, tramite l’attributo Table) ha la proprietà Name mappata sulla colonna Nome (stesso discorso per le colonne delle altre tabelle)
2) Il Developer ha una relazione con molti Project (API HasMany e WithMany) mappata (.Map) sulla tabella (ToTable) ProgettoSviluppatore utilizzando come chiavi (relazione a sinistra e destra) rispettivamente le colonne SviluppatoreId e ProgettoId
3) L’entità Developer ha una relazione con molti Skill, non obbligatoria (WithOptional) , utilizzando come chiave esterna la colonna SviluppatoreId della tabella competenze non esplicitamente indicata (IsIndipendent) nell’entità Skill.
Da sottolineare che tra Developer ed Architect è presenta una strategia di Table-per-type (TPT) Inheritance, in questo caso, automaticamente mappata. Cercheremo di vedere in dettaglio le varie strategie di mapping dell’ereditarietà tra oggetti e tabelle e i possibili tipi di relazione in uno dei prossimi post, insieme alla Validation.
Sicuramente, se si vorrà utilizzare l’approccio code first in progetti reali bisognerà fare molta pratica con Fluent API e Data Annotations per poter scrivere velocemente e senza troppi “errori” il mapping tra classi e tabelle di database. Lo studio continua, ma penso che in casi “complessi” IMHO l’approccio classico tramite Entity Model Designer aiuterebbe a focalizzare in modo veloce tutte le relazioni presenti tra gli oggetti del modello dati.