Sdílet prostřednictvím


Implementace dědičnosti pomocí Entity Frameworku v aplikaci ASP.NET MVC (8 ze 10)

Tom Dykstra

Ukázková webová aplikace Contoso University ukazuje, jak vytvářet aplikace ASP.NET MVC 4 pomocí entity Framework 5 Code First a sady Visual Studio 2012. Informace o sérii kurzů najdete v prvním kurzu v této sérii.

Poznámka

Pokud narazíte na problém, který nemůžete vyřešit, stáhněte si dokončenou kapitolu a zkuste problém reprodukovat. Obecně můžete najít řešení problému porovnáním kódu s dokončeným kódem. Informace o některých běžných chybách a jejich řešení najdete v tématu Chyby a alternativní řešení.

V předchozím kurzu jste zpracovávali výjimky souběžnosti. V tomto kurzu se dozvíte, jak implementovat dědičnost v datovém modelu.

V objektově orientovaném programování můžete pomocí dědičnosti eliminovat redundantní kód. V tomto kurzu změníte Instructor třídy a Student tak, aby byly odvozeny ze Person základní třídy, která obsahuje například LastName vlastnosti, které jsou společné pro instruktory i studenty. Nebudete přidávat ani měnit žádné webové stránky, ale změníte část kódu a tyto změny se automaticky projeví v databázi.

Dědičnost tabulky pro hierarchii versus dědičnost typu tabulky

V objektově orientovaném programování můžete pomocí dědičnosti usnadnit práci se souvisejícími třídami. Například Instructor třídy a Student v datovém School modelu sdílejí několik vlastností, což vede k redundantnímu kódu:

Snímky obrazovky zobrazující třídy Student a Instruktor se zvýrazněnými redundantními kódy

Předpokládejme, že chcete eliminovat nadbytečný kód pro vlastnosti, které jsou sdíleny entitami Instructor a Student . Mohli byste vytvořit Person základní třídu, která obsahuje pouze tyto sdílené vlastnosti, a potom nastavit Instructor entity a Student , aby se z této základní třídy dědily, jak je znázorněno na následujícím obrázku:

Snímek obrazovky znázorňující třídy Student a Instruktor odvozené z třídy Person

Tato struktura dědičnosti může být v databázi reprezentována několika způsoby. Můžete mít Person tabulku, která obsahuje informace o studentech i vyučujícím v jedné tabulce. Některé sloupce by se mohly vztahovat pouze na instruktory (HireDate), některé pouze na studenty (EnrollmentDate), některé pro oba (LastName, FirstName). Obvykle byste měli diskriminující sloupec, který označuje typ, který jednotlivé řádky představují. Například diskriminující sloupec může obsahovat "Instruktor" pro instruktory a "Student" pro studenty.

Snímek obrazovky znázorňující strukturu dědičnosti z třídy entity Person

Tento model generování struktury dědičnosti entit z jedné databázové tabulky se nazývá dědičnost TPH (table-per-hierarchy ).

Alternativou je, aby databáze vypadala spíše jako struktura dědičnosti. V tabulce můžete mít například jenom pole Person názvů a samostatné Instructor tabulky a Student tabulky s poli kalendářních dat.

Snímek obrazovky znázorňující nové databázové tabulky Instruktor a Student odvozené z třídy entity Person

Tento model vytvoření databázové tabulky pro každou třídu entity se nazývá dědičnost tabulky podle typu (TPT).

Vzory dědičnosti TPH obecně poskytují v Entity Frameworku lepší výkon než vzory dědičnosti TPT, protože vzory TPT mohou vést ke složitým dotazům spojení. Tento kurz ukazuje, jak implementovat dědičnost TPH. Provedete to provedením následujících kroků:

  • Vytvořte Person třídu a změňte třídy a Student takInstructor, aby byly odvozeny z Persontřídy .
  • Přidejte do třídy kontextu databáze kód mapování model-to-database.
  • Změňte InstructorID a StudentID odkazy v celém projektu na PersonID.

Vytvoření třídy Person

Poznámka: Po vytvoření níže uvedených tříd nebudete moct projekt zkompilovat, dokud neaktualizujete kontrolery, které tyto třídy používají.

Ve složce Models (Modely ) vytvořte soubor Person.cs a nahraďte kód šablony následujícím kódem:

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;
         }
      }
   }
}

V souboru Instructor.cs odvozujte Instructor třídu z Person třídy a odeberte pole klíče a názvu. Kód bude vypadat jako v následujícím příkladu:

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; }
    }
}

Proveďte podobné změny souboru Student.cs. Třída Student bude vypadat jako v následujícím příkladu:

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; }
    }
}

Přidání typu entity Person (Osoba) do modelu

V souboru SchoolContext.cs přidejte DbSet vlastnost pro Person typ entity:

public DbSet<Person> People { get; set; }

To je vše, co Entity Framework potřebuje ke konfiguraci dědičnosti jednotlivých tabulek v hierarchii. Jak uvidíte, při opětovném vytvoření databáze bude mít Person místo Student tabulek a Instructor tabulku.

Změna ID instruktora a ID studenta na PersonID

V souboru SchoolContext.cs změňte MapRightKey("InstructorID") v příkazu mapování Instructor-Course na MapRightKey("PersonID"):

modelBuilder.Entity<Course>()
    .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    .Map(t => t.MapLeftKey("CourseID")
    .MapRightKey("PersonID")
    .ToTable("CourseInstructor"));

Tato změna není nutná. Změní se jenom název sloupce InstructorID ve spojité tabulce M:N. Pokud necháte název jako InstructorID, aplikace bude i nadále fungovat správně. Tady je dokončený soubor SchoolContext.cs:

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"));
      }
   }
}

Dále je potřeba změnit InstructorID na PersonID a StudentID v PersonID celém projektu s výjimkou souborů migrace s časovým razítkem ve složce Migrations . K tomu najdete a otevřete jenom soubory, které je potřeba změnit, a pak u otevřených souborů provedete globální změnu. Jediný soubor ve složce Migrations , který byste měli změnit, je Migrations\Configuration.cs.

  1. Důležité

    Začněte zavřením všech otevřených souborů v sadě Visual Studio.

  2. V nabídce Úpravy klikněte na Najít a nahradit – Najít všechny soubory a pak vyhledejte všechny soubory v projektu, které obsahují InstructorID.

    Snímek obrazovky s oknem Najít a nahradit Zvýrazněná jsou zaškrtávací políčka Instruktor I D, Aktuální projekt, Rozlišovat velká a velká písmena, Shoda celých slov a Tlačítko Najít vše.

  3. Otevřete každý soubor v okně Najít výsledkys výjimkou<souborů migrace time-stamp>_.cs ve složce Migrations . Poklikáním na jeden řádek pro každý soubor.

    Snímek obrazovky s oknem Najít výsledky Soubory migrace s časovým razítkem jsou červeně přeškrtané.

  4. Otevřete dialogové okno Nahradit v souborech a změňte možnost Hledat na Všechny otevřené dokumenty.

  5. Pomocí dialogového okna Nahradit v souborech změňte vše InstructorID na PersonID.

    Snímek obrazovky s oknem Najít a nahradit Do textového pole Nahradit textem se zadává osoba I D.

  6. Vyhledejte všechny soubory v projektu, které obsahují StudentID.

  7. Otevřete každý soubor v okně Najít výsledkys výjimkou<souborů migrace time-stamp>_*.cs ve složce Migrations tím, že dvakrát kliknete na jeden řádek pro každý soubor.

    Snímek obrazovky s oknem Najít výsledky Soubory migrace časového razítka jsou přeškrtané.

  8. Otevřete dialogové okno Nahradit v souborech a změňte možnost Hledat na Všechny otevřené dokumenty.

  9. Pomocí dialogového okna Nahradit v souborech změňte vše StudentID na PersonID.

    Snímek obrazovky s oknem Najít a nahradit Zaškrtávací políčka Nahradit v souborech, Všechny otevřené dokumenty, Rozlišovat velká a malá písmena a Celá slova a tlačítko Nahradit vše jsou zvýrazněné.

  10. Sestavte projekt.

(Všimněte si, že to ukazuje nevýhoduclassnameID vzoru pro pojmenování primárních klíčů. Pokud byste pojmenovali ID primárních klíčů bez předpony názvu třídy, nebylo by teď potřeba přejmenovávat.)

Vytvoření a aktualizace souboru migrace

V konzole Správce balíčků (PMC) zadejte následující příkaz:

Add-Migration Inheritance

Spusťte příkaz Update-Database v PMC. Příkaz v tomto okamžiku selže, protože máme existující data, která migrace neumí zpracovat. Zobrazí se následující chyba:

Příkaz ALTER TABLE byl v konfliktu s omezením cizího klíče "FK_dbo. Department_dbo. Person_PersonID". Ke konfliktu došlo v databázi ContosoUniversity, tabulce dbo. Person", sloupec 'PersonID'.

Otevřené migrace< timestamp>_Inheritance.cs a nahraďte metodu Up následujícím kódem:

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");
}

Spusťte příkaz update-database znovu.

Poznámka

Při migraci dat a provádění změn schématu se můžou zobrazit další chyby. Pokud dojde k chybám migrace, které nemůžete vyřešit, můžete pokračovat v kurzu změnou připojovací řetězec v souboru Web.config nebo odstraněním databáze. Nejjednodušší je přejmenovat databázi v souboruWeb.config . Změňte například název databáze na CU_test, jak je znázorněno v následujícím příkladu:

<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" />

V případě nové databáze se žádná data nemigrují a update-database příkaz se s větší pravděpodobností dokončí bez chyb. Pokyny k odstranění databáze najdete v tématu Odstranění databáze ze sady Visual Studio 2012. Pokud použijete tento přístup, abyste mohli pokračovat v kurzu, přeskočte krok nasazení na konci tohoto kurzu, protože nasazené lokalitě by se při automatickém spuštění migrací zobrazila stejná chyba. Pokud chcete vyřešit chybu migrace, nejlepším prostředkem je jedno z fór Entity Framework nebo StackOverflow.com.

Testování

Spusťte web a vyzkoušejte různé stránky. Všechno funguje stejně jako předtím.

V Průzkumníku serveru rozbalte SchoolContext a pak Tabulky a uvidíte, že tabulky Student a Instruktor byly nahrazeny tabulkou Osoba . Rozbalte tabulku Person (Osoba ) a uvidíte, že obsahuje všechny sloupce, které bývaly v tabulkách Student a Instruktor .

Snímek obrazovky s oknem Průzkumníka serveru Karty Datová připojení, Kontext školy a Tabulky jsou rozbalené a zobrazují tabulku Person (Osoba).

Klikněte pravým tlačítkem myši na tabulku Person (Osoba) a potom kliknutím na Show Table Data (Zobrazit data tabulky ) zobrazte diskriminující sloupec.

Snímek obrazovky s tabulkou Person (Osoba) Je zvýrazněný název diskriminujícího sloupce.

Následující diagram znázorňuje strukturu nové školní databáze:

Snímek obrazovky znázorňující diagram školní databáze

Souhrn

Dědičnost tabulek na hierarchii je teď implementovaná pro Persontřídy , Studenta Instructor . Další informace o této a dalších strukturách dědičnosti najdete v tématu Strategie mapování dědičnosti na blogu Mortezy Manavi. V dalším kurzu uvidíte některé způsoby implementace schémat úložiště a jednotek práce.

Odkazy na další prostředky Entity Framework najdete v mapě obsahu ASP.NET Data Access.