Implementace dědičnosti pomocí Entity Frameworku v aplikaci ASP.NET MVC (8 ze 10)
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:
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:
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.
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.
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 aStudent
takInstructor
, aby byly odvozeny zPerson
třídy . - Přidejte do třídy kontextu databáze kód mapování model-to-database.
- Změňte
InstructorID
aStudentID
odkazy v celém projektu naPersonID
.
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.
-
Důležité
Začněte zavřením všech otevřených souborů v sadě Visual Studio.
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
.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.
Otevřete dialogové okno Nahradit v souborech a změňte možnost Hledat na Všechny otevřené dokumenty.
Pomocí dialogového okna Nahradit v souborech změňte vše
InstructorID
naPersonID.
Vyhledejte všechny soubory v projektu, které obsahují
StudentID
.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.
Otevřete dialogové okno Nahradit v souborech a změňte možnost Hledat na Všechny otevřené dokumenty.
Pomocí dialogového okna Nahradit v souborech změňte vše
StudentID
naPersonID
.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 .
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.
Následující diagram znázorňuje strukturu nové školní databáze:
Souhrn
Dědičnost tabulek na hierarchii je teď implementovaná pro Person
třídy , Student
a 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.