Condividi tramite


Esercitazione: Implementare l'ereditarietà - ASP.NET MVC con EF Core

Nell'esercitazione precedente sono state presentate le eccezioni di concorrenza. In questa esercitazione viene illustrato come implementare l'ereditarietà nel modello di dati.

Nella programmazione orientata a oggetti è possibile usare l'ereditarietà per facilitare il riutilizzo del codice. 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.

In questa esercitazione:

  • Eseguire il mapping dell'ereditarietà al database
  • Creare la classe Person
  • Aggiornare Student e Instructor
  • Aggiungere Person al modello
  • Creare e aggiornare le migrazioni
  • Testare l'implementazione

Prerequisiti

Eseguire il mapping dell'ereditarietà al database

Le classi Instructor e Student nel modello di dati School presentano molte proprietà identiche:

Student and Instructor classes

Si supponga di voler eliminare il codice ridondante per le proprietà condivise dalle entità Instructor e Student. Oppure che si desideri scrivere un servizio in grado di formattare i nomi senza sapere se il nome appartiene a un docente o a uno studente. È possibile creare una classe di base Person che contiene solo le proprietà condivise e quindi fare in modo che le classi Instructor e Student ereditino da questa classe di base, come illustrato nella figura seguente:

Student and Instructor classes deriving from Person class

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 riguardare solo i docenti (HireDate), altre solo gli studenti (EnrollmentDate) e altre ancora entrambi (LastName, FirstName). Per indicare il tipo rappresentato da ogni riga viene in genere usata una colonna discriminante. La colonna discriminante può ad esempio indicare "Instructor" per i docenti e "Student" per gli studenti.

Table-per-hierarchy example

Questo modello di generazione di una struttura di ereditarietà di entità da una singola tabella di database è detta ereditarietà della tabella per gerarchia (TPH).

Un'alternativa consiste nel rendere il database più simile alla struttura di ereditarietà. Ad esempio, è possibile avere solo i campi nome nella Person tabella e avere tabelle separate Instructor e Student con i campi data.

Avviso

La tabella per tipo (TPT) non è supportata dalla EF Core versione 3.x, ma è stata implementata nella EF Core versione 5.0.

Table-per-type inheritance

Questo modello di creazione di una tabella di database per ogni classe di entità è denominato ereditarietà di tabella per tipo (TPT).

Un'altra opzione consiste nell'eseguire il mapping di tutti i tipi non astratti a singole tabelle. Tutte le proprietà di una classe, incluse le proprietà ereditate, eseguono il mapping alle colonne della tabella corrispondente. Questo modello è denominato ereditarietà TPC (Table-per-Concrete Class). Se è stata implementata l'ereditarietà TPC per le Personclassi , Studente Instructor come illustrato in precedenza, le Student tabelle e Instructor non avranno un aspetto diverso dopo l'implementazione dell'ereditarietà rispetto a quanto fatto in precedenza.

I criteri tabella per tipo concreto e tabella per gerarchia offrono in genere prestazioni migliori rispetto ai criteri tabella per tipo perché questi ultimi possono generare query join complesse.

Questa esercitazione illustra come implementare l'ereditarietà tabella per gerarchia. Il criterio di ereditarietà tabella per gerarchia è l'unico supportato da Entity Framework Core. Verranno eseguite le operazioni di creazione di una classe Person, modifica delle classi Instructor e Student in modo che derivino da Person, aggiunta della nuova classe a DbContext e creazione di una migrazione.

Suggerimento

È consigliabile salvare una copia del progetto prima di apportare le modifiche seguenti. In caso di problemi e se fosse necessario ricominciare da capo, sarà più facile iniziare dal progetto salvato invece di annullare i passaggi eseguiti per questa esercitazione o tornare all'inizio dell'intera serie.

Creare la classe Person

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
    {
        public int ID { get; set; }

        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }
    }
}

Aggiornare Student e Instructor

In Instructor.csderivare la classe Instructor dalla classe Person e rimuovere i campi chiave e nome. Il codice sarà simile all'esempio seguente:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

Apportare le stesse modifiche in Student.cs.

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student : Person
    {
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }


        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

Aggiungere Person al modello

Aggiungere il tipo di entità Person a SchoolContext.cs. Le nuove righe sono evidenziate.

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Data
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollments { get; set; }
        public DbSet<Student> Students { get; set; }
        public DbSet<Department> Departments { get; set; }
        public DbSet<Instructor> Instructors { get; set; }
        public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
        public DbSet<CourseAssignment> CourseAssignments { get; set; }
        public DbSet<Person> People { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.Entity<Course>().ToTable("Course");
            modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
            modelBuilder.Entity<Student>().ToTable("Student");
            modelBuilder.Entity<Department>().ToTable("Department");
            modelBuilder.Entity<Instructor>().ToTable("Instructor");
            modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
            modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
            modelBuilder.Entity<Person>().ToTable("Person");

            modelBuilder.Entity<CourseAssignment>()
                .HasKey(c => new { c.CourseID, c.InstructorID });
        }
    }
}

L'ereditarietà tabella per gerarchia in Entity Framework è stata configurata. Come si vedrà, quando il database verrà aggiornato conterrà una tabella Person invece delle tabelle Student e Instructor.

Creare e aggiornare le migrazioni

Salvare le modifiche e compilare il progetto. Quindi aprire la finestra di comando nella cartella del progetto e immettere il comando seguente:

dotnet ef migrations add Inheritance

Non eseguire ancora il comando database update. Questo comando determinerà la perdita di dati poiché eliminerà la tabella Instructor e rinominerà la tabella Student in Person. Per mantenere i dati esistenti è necessario specificare codice personalizzato.

Aprire Migrations/<timestamp>_Inheritance.cs e sostituire il Up metodo con il codice seguente:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.DropForeignKey(
        name: "FK_Enrollment_Student_StudentID",
        table: "Enrollment");

    migrationBuilder.DropIndex(name: "IX_Enrollment_StudentID", table: "Enrollment");

    migrationBuilder.RenameTable(name: "Instructor", newName: "Person");
    migrationBuilder.AddColumn<DateTime>(name: "EnrollmentDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<string>(name: "Discriminator", table: "Person", nullable: false, maxLength: 128, defaultValue: "Instructor");
    migrationBuilder.AlterColumn<DateTime>(name: "HireDate", table: "Person", nullable: true);
    migrationBuilder.AddColumn<int>(name: "OldId", table: "Person", nullable: true);

    // Copy existing Student data into new Person table.
    migrationBuilder.Sql("INSERT INTO dbo.Person (LastName, FirstName, HireDate, EnrollmentDate, Discriminator, OldId) SELECT LastName, FirstName, null AS HireDate, EnrollmentDate, 'Student' AS Discriminator, ID AS OldId FROM dbo.Student");
    // Fix up existing relationships to match new PK's.
    migrationBuilder.Sql("UPDATE dbo.Enrollment SET StudentId = (SELECT ID FROM dbo.Person WHERE OldId = Enrollment.StudentId AND Discriminator = 'Student')");

    // Remove temporary key
    migrationBuilder.DropColumn(name: "OldID", table: "Person");

    migrationBuilder.DropTable(
        name: "Student");

    migrationBuilder.CreateIndex(
         name: "IX_Enrollment_StudentID",
         table: "Enrollment",
         column: "StudentID");

    migrationBuilder.AddForeignKey(
        name: "FK_Enrollment_Person_StudentID",
        table: "Enrollment",
        column: "StudentID",
        principalTable: "Person",
        principalColumn: "ID",
        onDelete: ReferentialAction.Cascade);
}

Questo codice esegue le attività di aggiornamento del database seguenti:

  • Rimuove i vincoli di chiave esterna e gli indici che puntano alla tabella Student.

  • Rinomina la tabella Instructor in Person e apporta le modifiche necessarie perché sia in grado di archiviare i dati Student:

  • Aggiunge un elemento EnrollmentDate che ammette i valori Null per gli studenti.

  • Aggiunge una colonna discriminante che indica se una riga è per uno studente o un docente.

  • Modifica HireDate in modo che ammetta i valori Null poiché le righe degli studenti non includono date di assunzione.

  • Aggiunge un campo temporaneo che sarà usato per aggiornare le chiavi esterne che puntano agli studenti. Quando si copiano studenti nella tabella Person, questi otterranno nuovi valori di chiave primaria.

  • Copia i dati dalla tabella Student alla tabella Person. Ciò comporta l'assegnazione di nuovi valori di chiave primaria agli studenti.

  • Corregge i valori di chiave esterna che puntano agli studenti.

  • Ricrea i vincoli di chiave esterna e gli indici che ora puntano alla tabella Person.

Se come tipo di chiave primaria fosse stato usato GUID anziché Integer, non sarebbe necessario modificare i valori di chiave primaria degli studenti e molti di questi passaggi avrebbero potuto essere omessi.

Eseguire il comando database update:

dotnet ef database update

In un sistema di produzione è necessario apportare le modifiche corrispondenti al Down metodo nel caso in cui sia mai stato necessario usarlo per tornare alla versione precedente del database. Per questa esercitazione non si usa il Down metodo .

Nota

Quando si apportano modifiche allo schema in un database con dati esistenti è possibile che si verifichino altri errori. Se si verificano errori di migrazione che non si riesce a risolvere, è possibile modificare il nome del database nella stringa di connessione o eliminare il database. Un nuovo database non contiene dati di cui eseguire la migrazione e ci sono maggiori probabilità che il comando update-database venga completato senza errori. Per eliminare il database, usare SSOX o eseguire il comando dell’interfaccia della riga di comando database drop.

Testare l'implementazione

Eseguire l'app e provare diverse pagine. Tutto funziona come in precedenza.

In Esplora oggetti di SQL Server espandere Connessioni dati/SchoolContext e quindi Tabelle. Si osserverà che le tabelle Student e Instructor sono state sostituite da una tabella Person. Dopo aver aperto la tabella Person in Progettazione tabelle si noterà che contiene tutte le colonne che in precedenza erano presenti nelle tabelle Student e Instructor.

Person table in SSOX

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

Person table in SSOX - table data

Ottenere il codice

Scaricare o visualizzare l'applicazione completata.

Risorse aggiuntive

Per altre informazioni sull'ereditarietà in Entity Framework Core, vedere Ereditarietà.

Passaggi successivi

In questa esercitazione:

  • Eseguire il mapping dell'ereditarietà al database
  • Creare la classe Person
  • Aggiornare Student e Instructor
  • Aggiungere Person al modello
  • Creare e aggiornare le migrazioni
  • Testare l'implementazione

Passare all'esercitazione successiva per apprendere come gestire diversi scenari di Entity Framework relativamente avanzati.