Condividi tramite


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:

Screenshot che mostrano le classi Student e Instructor con i codici ridondanti evidenziati.

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:

Screenshot che mostra le classi Student e Instructor derivate dalla classe Person.

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.

Screenshot che mostra la struttura di ereditarietà dalla classe di entità Person.

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.

Screenshot che mostra le nuove tabelle di database Instructor e Student derivate dalla classe di entità Person.

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 le Instructor classi e Student per derivare da Person.
  • Aggiungere il codice di mapping da modello a database alla classe di contesto del database.
  • Modificare InstructorID e StudentID fare riferimento in tutto il progetto in PersonID.

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.

  1. Importante

    Iniziare chiudendo tutti i file aperti in Visual Studio.

  2. Fare clic su Trova e sostituisci - Trova tutti i file nel menu Modifica , quindi cercare tutti i file nel progetto che contengono InstructorID.

    Screenshot che mostra la finestra Trova e sostituisci. Le caselle di controllo Instructor I D, Current Project, Match case e Match whole word (Trova tutto) e Find All (Trova tutto) sono tutte evidenziate.

  3. 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.

    Screenshot che mostra la finestra Risultati ricerca. I file di migrazione timestamp vengono incrociati in rosso.

  4. Aprire la finestra di dialogo Sostituisci nei file e impostare Cerca inTutti i documenti aperti.

  5. Usare la finestra di dialogo Sostituisci nei file per modificare tutti in InstructorIDPersonID.

    Screenshot che mostra la finestra Trova e sostituisci. La persona D viene immessa nel campo Sostituisci con testo.

  6. Trovare tutti i file nel progetto che contengono StudentID.

  7. 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.

    Screenshot che mostra la finestra Risultati ricerca. I file di migrazione del timestamp vengono superati.

  8. Aprire la finestra di dialogo Sostituisci nei file e impostare Cerca inTutti i documenti aperti.

  9. Usare la finestra di dialogo Sostituisci nei file per modificare tutti in StudentIDPersonID.

    Screenshot che mostra la finestra Trova e sostituisci. Sostituire nelle caselle di controllo File, Tutti i documenti aperti, Maiuscole/minuscole e Trova corrispondenza intere parole e il pulsante Sostituisci tutto sono evidenziati.

  10. 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 .

Screenshot che mostra la finestra Esplora server. Le schede Connessioni dati, Contesto dell'istituto di istruzione e Tabelle vengono espanse per visualizzare la tabella Person.

Fare clic con il pulsante destro del mouse sulla tabella Person e quindi su Mostra dati tabella per vedere la colonna discriminante.

Screenshot che mostra la tabella Person. Il nome della colonna Discriminatorio è evidenziato.

Il diagramma seguente illustra la struttura del nuovo database School:

Screenshot che mostra il diagramma del database School.

Riepilogo

L'ereditarietà delle tabelle per gerarchia è stata ora implementata per le Personclassi , Studente 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.