Implementazione dell'ereditarietà con Entity Framework in un'applicazione MVC ASP.NET (8 di 10)
di Tom Dykstra
L'applicazione Web di esempio Contoso University illustra come creare ASP.NET applicazioni MVC 4 usando Entity Framework 5 Code First e Visual Studio 2012. Per informazioni sulla serie di esercitazioni, vedere la prima esercitazione della serie.
Nota
Se si verifica un problema che non è possibile risolvere, scaricare il capitolo completato e provare a riprodurre il problema. In genere è possibile trovare la soluzione al problema confrontando il codice con il codice completato. Per alcuni errori comuni e come risolverli, vedere Errori e soluzioni alternative.
Nell'esercitazione precedente sono state gestite le eccezioni di concorrenza. In questa esercitazione viene illustrato come implementare l'ereditarietà nel modello di dati.
Nella programmazione orientata agli oggetti è possibile usare l'ereditarietà per eliminare il codice ridondante. In questa esercitazione verranno modificate le classi Instructor
e Student
in modo che derivino da una classe di base Person
contenente proprietà quali LastName
comuni a docenti e studenti. Non verranno aggiunte o modificate pagine Web, ma si modificherà parte del codice e le modifiche verranno automaticamente riflesse nel database.
Ereditarietà tabella per gerarchia e tabella per tipo
Nella programmazione orientata agli oggetti è possibile usare l'ereditarietà per semplificare l'uso delle classi correlate. Ad esempio, le Instructor
classi e Student
nel School
modello di dati condividono diverse proprietà, che generano codice ridondante:
Si supponga di voler eliminare il codice ridondante per le proprietà condivise dalle entità Instructor
e Student
. È possibile creare una Person
classe di base contenente solo le proprietà condivise, quindi impostare le Instructor
entità e Student
ereditano da tale classe di base, come illustrato nella figura seguente:
Questa struttura di ereditarietà può essere rappresentata nel database in diversi modi. È possibile avere una Person
tabella che include informazioni su studenti e insegnanti in un'unica tabella. Alcune colonne possono essere applicate solo agli insegnanti (HireDate
), alcuni solo agli studenti (EnrollmentDate
), alcuni a entrambi (LastName
, FirstName
). In genere, è disponibile una colonna discriminatoria per indicare il tipo rappresentato da ogni riga. La colonna discriminante può ad esempio indicare "Instructor" per i docenti e "Student" per gli studenti.
Questo modello di generazione di una struttura di ereditarietà delle entità da una singola tabella di database è detta ereditarietà di tabella per gerarchia (TPH).
Un'alternativa consiste nel rendere il database più simile alla struttura di ereditarietà. Ad esempio, è possibile avere solo i campi del nome nella Person
tabella e avere tabelle separate Instructor
e Student
con i campi data.
Questo modello di creazione di una tabella di database per ogni classe di entità viene chiamato ereditarietà di tabella per tipo (TPT).
I modelli di ereditarietà TPH offrono in genere prestazioni migliori in Entity Framework rispetto ai modelli di ereditarietà TPT, perché i modelli TPT possono comportare query di join complesse. Questa esercitazione illustra come implementare l'ereditarietà tabella per gerarchia. A tale scopo, seguire questa procedura:
- Creare una
Person
classe e modificare leInstructor
classi eStudent
per derivare daPerson
. - Aggiungere il codice di mapping da modello a database alla classe di contesto del database.
- Modificare
InstructorID
eStudentID
fare riferimento in tutto il progetto inPersonID
.
Creazione della classe Person
Nota: non sarà possibile compilare il progetto dopo aver creato le classi seguenti finché non si aggiornano i controller che usano queste classi.
Nella cartella Models creare Person.cs e sostituire il codice del modello con il codice seguente:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public abstract class Person
{
[Key]
public int PersonID { get; set; }
[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]
[StringLength(50, MinimumLength = 1)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50, MinimumLength = 2, ErrorMessage = "First name must be between 2 and 50 characters.")]
public string FirstMidName { get; set; }
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
}
}
In Instructor.cs derivare la Instructor
classe dalla Person
classe e rimuovere i campi chiave e nome. Il codice sarà simile all'esempio seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Instructor : Person
{
[DataType(DataType.Date)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
Apportare modifiche simili a Student.cs. La Student
classe sarà simile all'esempio seguente:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student : Person
{
[DataType(DataType.Date)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
Aggiunta del tipo di entità Person al modello
In SchoolContext.cs aggiungere una DbSet
proprietà per il Person
tipo di entità:
public DbSet<Person> People { get; set; }
L'ereditarietà tabella per gerarchia in Entity Framework è stata configurata. Come si vedrà, quando il database viene ricreato, avrà una Person
tabella al posto delle Student
tabelle e Instructor
.
Modifica di InstructorID e StudentID in PersonID
In SchoolContext.cs, nell'istruzione di mapping Instructor-Course passare MapRightKey("InstructorID")
a MapRightKey("PersonID")
:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("PersonID")
.ToTable("CourseInstructor"));
Questa modifica non è necessaria; modifica solo il nome della colonna InstructorID nella tabella join molti-a-molti. Se il nome è stato lasciato come InstructorID, l'applicazione funzionerà comunque correttamente. Di seguito è riportato il file SchoolContext.cs completato:
using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace ContosoUniversity.DAL
{
public class SchoolContext : DbContext
{
public DbSet<Course> Courses { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<Person> People { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("PersonID")
.ToTable("CourseInstructor"));
}
}
}
Successivamente è necessario passare InstructorID
a PersonID
e StudentID
in PersonID
tutto il progetto , ad eccezione dei file delle migrazioni con timestamp nella cartella Migrazioni . A tale scopo, è possibile trovare e aprire solo i file che devono essere modificati, quindi eseguire una modifica globale sui file aperti. L'unico file nella cartella Migrations da modificare è Migrations\Configuration.cs.
-
Importante
Iniziare chiudendo tutti i file aperti in Visual Studio.
Fare clic su Trova e sostituisci - Trova tutti i file nel menu Modifica , quindi cercare tutti i file nel progetto che contengono
InstructorID
.Aprire ogni file nella finestra Risultati ricercaad eccezione dei <file di migrazione time-stamp>_.cs nella cartella Migrazioni facendo doppio clic su una riga per ogni file.
Aprire la finestra di dialogo Sostituisci nei file e impostare Cerca inTutti i documenti aperti.
Usare la finestra di dialogo Sostituisci nei file per modificare tutti in
InstructorID
PersonID.
Trovare tutti i file nel progetto che contengono
StudentID
.Aprire ogni file nella finestra Risultati ricercaad eccezione dei <file di migrazione time-stamp>_*.cs nella cartella Migrazioni facendo doppio clic su una riga per ogni file.
Aprire la finestra di dialogo Sostituisci nei file e impostare Cerca inTutti i documenti aperti.
Usare la finestra di dialogo Sostituisci nei file per modificare tutti in
StudentID
PersonID
.Compilare il progetto.
Si noti che questo dimostra uno svantaggio del classnameID
modello per la denominazione delle chiavi primarie. Se l'ID delle chiavi primarie è stato denominato senza anteporre il nome della classe, non sarebbe necessario rinominare.
Creare e aggiornare un file di migrazioni
Nella console di Gestione pacchetti immettere il comando seguente:
Add-Migration Inheritance
Eseguire il Update-Database
comando in PMC. Il comando avrà esito negativo a questo punto perché sono presenti dati esistenti che le migrazioni non sanno come gestire. Viene visualizzato l'errore seguente:
L'istruzione ALTER TABLE è in conflitto con il vincolo FOREIGN KEY "FK_dbo. Department_dbo. Person_PersonID". Il conflitto si è verificato nel database "ContosoUniversity", tabella "dbo. Person", colonna 'PersonID'.
Aprire Migrazioni< timestamp>_Inheritance.cs e sostituire il Up
metodo con il codice seguente:
public override void Up()
{
DropForeignKey("dbo.Department", "InstructorID", "dbo.Instructor");
DropForeignKey("dbo.OfficeAssignment", "InstructorID", "dbo.Instructor");
DropForeignKey("dbo.Enrollment", "StudentID", "dbo.Student");
DropForeignKey("dbo.CourseInstructor", "InstructorID", "dbo.Instructor");
DropIndex("dbo.Department", new[] { "InstructorID" });
DropIndex("dbo.OfficeAssignment", new[] { "InstructorID" });
DropIndex("dbo.Enrollment", new[] { "StudentID" });
DropIndex("dbo.CourseInstructor", new[] { "InstructorID" });
RenameColumn(table: "dbo.Department", name: "InstructorID", newName: "PersonID");
RenameColumn(table: "dbo.OfficeAssignment", name: "InstructorID", newName: "PersonID");
RenameColumn(table: "dbo.Enrollment", name: "StudentID", newName: "PersonID");
RenameColumn(table: "dbo.CourseInstructor", name: "InstructorID", newName: "PersonID");
CreateTable(
"dbo.Person",
c => new
{
PersonID = c.Int(nullable: false, identity: true),
LastName = c.String(maxLength: 50),
FirstName = c.String(maxLength: 50),
HireDate = c.DateTime(),
EnrollmentDate = c.DateTime(),
Discriminator = c.String(nullable: false, maxLength: 128),
OldId = c.Int(nullable: false)
})
.PrimaryKey(t => t.PersonID);
// Copy existing Student and Instructor data into new Person table.
Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, StudentId AS OldId FROM dbo.Student");
Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, HireDate, null AS EnrollmentDate, 'Instructor' AS Discriminator, InstructorId AS OldId FROM dbo.Instructor");
// Fix up existing relationships to match new PK's.
Sql("UPDATE dbo.Enrollment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Enrollment.PersonId AND Discriminator = 'Student')");
Sql("UPDATE dbo.Department SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = Department.PersonId AND Discriminator = 'Instructor')");
Sql("UPDATE dbo.OfficeAssignment SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = OfficeAssignment.PersonId AND Discriminator = 'Instructor')");
Sql("UPDATE dbo.CourseInstructor SET PersonId = (SELECT PersonId FROM dbo.Person WHERE OldId = CourseInstructor.PersonId AND Discriminator = 'Instructor')");
// Remove temporary key
DropColumn("dbo.Person", "OldId");
AddForeignKey("dbo.Department", "PersonID", "dbo.Person", "PersonID");
AddForeignKey("dbo.OfficeAssignment", "PersonID", "dbo.Person", "PersonID");
AddForeignKey("dbo.Enrollment", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
AddForeignKey("dbo.CourseInstructor", "PersonID", "dbo.Person", "PersonID", cascadeDelete: true);
CreateIndex("dbo.Department", "PersonID");
CreateIndex("dbo.OfficeAssignment", "PersonID");
CreateIndex("dbo.Enrollment", "PersonID");
CreateIndex("dbo.CourseInstructor", "PersonID");
DropTable("dbo.Instructor");
DropTable("dbo.Student");
}
Eseguire di nuovo il comando update-database
.
Nota
È possibile ottenere altri errori durante la migrazione dei dati e apportare modifiche allo schema. Se si verificano errori di migrazione non risolti, è possibile continuare con l'esercitazione modificando il stringa di connessione nel file Web.config o eliminando il database. L'approccio più semplice consiste nel rinominare il database nel file Web.config . Ad esempio, modificare il nome del database in CU_test come illustrato nell'esempio seguente:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;
Integrated Security=SSPI;AttachDBFilename=|DataDirectory|\CU_Test.mdf"
providerName="System.Data.SqlClient" />
Con un nuovo database, non sono presenti dati di cui eseguire la migrazione e il update-database
comando è molto più probabile che venga completato senza errori. Per istruzioni su come eliminare il database, vedere Come eliminare un database da Visual Studio 2012. Se si accetta questo approccio per continuare con l'esercitazione, ignorare il passaggio di distribuzione alla fine di questa esercitazione, poiché il sito distribuito riceverà lo stesso errore quando esegue automaticamente le migrazioni. Se si vuole risolvere un errore di migrazione, la risorsa migliore è uno dei forum di Entity Framework o StackOverflow.com.
Test
Eseguire il sito e provare varie pagine. Tutto funziona come in precedenza.
In Esplora server espandere SchoolContext e quindi Tabelle e si noterà che le tabelle Student e Instructor sono state sostituite da una tabella Person . Espandere la tabella Person e si noterà che contiene tutte le colonne usate per essere presenti nelle tabelle Student e Instructor .
Fare clic con il pulsante destro del mouse sulla tabella Person e quindi su Mostra dati tabella per vedere la colonna discriminante.
Il diagramma seguente illustra la struttura del nuovo database School:
Riepilogo
L'ereditarietà delle tabelle per gerarchia è stata ora implementata per le Person
classi , Student
e Instructor
. Per altre informazioni su questa e altre strutture di ereditarietà, vedere Strategie di mapping dell'ereditarietà nel blog di Morteza Manavi. Nell'esercitazione successiva verranno illustrati alcuni modi per implementare il repository e l'unità di modelli di lavoro.
I collegamenti ad altre risorse di Entity Framework sono disponibili nella mappa del contenuto ASP.NET accesso ai dati.