Implementowanie dziedziczenia za pomocą programu Entity Framework w aplikacji MVC ASP.NET (8 z 10)
Autor : Tom Dykstra
Przykładowa aplikacja internetowa Contoso University pokazuje, jak utworzyć ASP.NET aplikacje MVC 4 przy użyciu platformy Entity Framework 5 Code First i programu Visual Studio 2012. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek z serii.
Uwaga
Jeśli napotkasz problem, którego nie możesz rozwiązać, pobierz ukończony rozdział i spróbuj odtworzyć problem. Zazwyczaj rozwiązanie problemu można znaleźć, porównując kod z ukończonym kodem. Niektóre typowe błędy i sposoby ich rozwiązywania można znaleźć w temacie Błędy i obejścia.
W poprzednim samouczku obsłużyliśmy wyjątki współbieżności. W tym samouczku pokazano, jak zaimplementować dziedziczenie w modelu danych.
W programowaniu obiektowym można użyć dziedziczenia, aby wyeliminować nadmiarowy kod. W tym samouczku zmienisz Instructor
klasy i Student
tak, aby pochodziły one z klasy bazowej Person
, która zawiera właściwości, takie jak LastName
wspólne dla instruktorów i studentów. Nie dodasz ani nie zmienisz żadnych stron sieci Web, ale zmienisz część kodu, a te zmiany zostaną automatycznie odzwierciedlone w bazie danych.
Dziedziczenie tabeli na hierarchię w porównaniu z dziedziczeniem tabeli na typ
W programowaniu obiektowym można użyć dziedziczenia, aby ułatwić pracę z powiązanymi klasami. Na przykład Instructor
klasy i Student
w School
modelu danych współdzielą kilka właściwości, co powoduje nadmiarowy kod:
Załóżmy, że chcesz wyeliminować nadmiarowy kod dla właściwości udostępnianych przez Instructor
jednostki i Student
. Można utworzyć klasę bazową zawierającą Person
tylko te właściwości udostępnione, a następnie sprawić, aby Instructor
jednostki i Student
dziedziczyły po tej klasie bazowej, jak pokazano na poniższej ilustracji:
Istnieje kilka sposobów reprezentacji tej struktury dziedziczenia w bazie danych. Możesz mieć tabelę zawierającą Person
informacje o uczniach i instruktorach w jednej tabeli. Niektóre kolumny mogą być stosowane tylko do instruktorów (HireDate
), niektóre tylko dla uczniów (EnrollmentDate
), niektóre do obu (LastName
, FirstName
). Zazwyczaj istnieje kolumna dyskryminująca wskazująca typ reprezentowany przez każdy wiersz. Na przykład kolumna dyskryminująca może mieć wartość "Instruktor" dla instruktorów i "Student" dla uczniów.
Ten wzorzec generowania struktury dziedziczenia jednostki z pojedynczej tabeli bazy danych jest nazywany dziedziczeniem TPH ( table-per-hierarchy ).
Alternatywą jest uczynienie bazy danych bardziej podobną do struktury dziedziczenia. Na przykład można mieć tylko pola nazw w Person
tabeli i mieć oddzielne Instructor
tabele z Student
polami daty.
Ten wzorzec tworzenia tabeli bazy danych dla każdej klasy jednostki jest nazywany dziedziczeniem typu (TPT).
Wzorce dziedziczenia TPH zwykle zapewniają lepszą wydajność w programie Entity Framework niż wzorce dziedziczenia TPT, ponieważ wzorce TPT mogą powodować złożone zapytania sprzężenia. W tym samouczku pokazano, jak zaimplementować dziedziczenie TPH. W tym celu należy wykonać następujące czynności:
- Utwórz klasę
Person
i zmieńInstructor
klasy iStudent
, aby pochodziły z klasyPerson
. - Dodaj kod mapowania modelu do bazy danych do klasy kontekstu bazy danych.
- Zmień
InstructorID
odwołania iStudentID
w całym projekcie naPersonID
.
Tworzenie klasy person
Uwaga: nie będzie można skompilować projektu po utworzeniu poniższych klas, dopóki nie zaktualizujesz kontrolerów używających tych klas.
W folderze Models utwórz plik Person.cs i zastąp kod szablonu następującym kodem:
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;
}
}
}
}
W pliku Instructor.cs utwórz klasę Instructor
z Person
klasy i usuń pola klucza i nazwy. Kod będzie wyglądać podobnie do następującego przykładu:
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; }
}
}
Wprowadź podobne zmiany w pliku Student.cs. Klasa Student
będzie wyglądać podobnie do następującego przykładu:
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; }
}
}
Dodawanie typu jednostki osoby do modelu
W pliku SchoolContext.cs dodaj DbSet
właściwość dla Person
typu jednostki:
public DbSet<Person> People { get; set; }
Jest to wszystko, którego potrzebuje program Entity Framework w celu skonfigurowania dziedziczenia tabeli na hierarchię. Jak zobaczysz, po ponownym utworzeniu bazy danych będzie ona zawierać tabelę Person
zamiast Student
tabel i Instructor
.
Zmiana identyfikatora instruktora i identyfikatora studenta na PersonID
W pliku SchoolContext.cs w instrukcji mapowania Instructor-Course zmień wartość MapRightKey("InstructorID")
na MapRightKey("PersonID")
:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("PersonID")
.ToTable("CourseInstructor"));
Ta zmiana nie jest wymagana; po prostu zmienia nazwę kolumny InstructorID w tabeli sprzężenia wiele-do-wielu. Jeśli nazwa została pozostawiona jako Identyfikator instruktora, aplikacja nadal będzie działać poprawnie. Oto ukończona plik 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"));
}
}
}
Następnie należy zmienić wartość InstructorID
na i StudentID
na PersonID
PersonID
w całym projekcie z wyjątkiem plików migracji z sygnaturą czasową w folderze Migrations. W tym celu znajdziesz i otworzysz tylko pliki, które należy zmienić, a następnie przeprowadź zmianę globalną w otwartych plikach. Jedynym plikiem w folderze Migrations , który należy zmienić, jest Migrations\Configuration.cs.
-
Ważne
Zacznij od zamknięcia wszystkich otwartych plików w programie Visual Studio.
Kliknij pozycję Znajdź i zamień — znajdź wszystkie pliki w menu Edycja , a następnie wyszukaj wszystkie pliki w projekcie zawierającym
InstructorID
plik .Otwórz każdy plik w oknie Znajdź wynikiz wyjątkiem<plików migracji sygnatura>_czasowa.cs w folderze Migrations , klikając dwukrotnie jeden wiersz dla każdego pliku.
Otwórz okno dialogowe Zamień w plikach i zmień pozycję Look in na All Open Documents (Wyszukiwanie we wszystkich otwartych dokumentach).
Użyj okna dialogowego Zamień w plikach, aby zmienić wszystko
InstructorID
naPersonID.
Znajdź wszystkie pliki w projekcie, które zawierają
StudentID
element .Otwórz każdy plik w oknie Znajdź wynikiz wyjątkiem<plików migracji sygnatury> czasowej*.cs w folderze Migrations, klikając dwukrotnie jeden wiersz dla każdego pliku.
Otwórz okno dialogowe Zamień w plikach i zmień pozycję Look in na All Open Documents (Wyszukiwanie we wszystkich otwartych dokumentach).
Użyj okna dialogowego Zamień w plikach , aby zmienić wszystko
StudentID
naPersonID
.Skompiluj projekt.
(Należy pamiętać, że jest to wadąclassnameID
wzorca nazewnictwa kluczy podstawowych. Jeśli nazwano identyfikator kluczy podstawowych bez prefiksu nazwy klasy, nie trzeba teraz zmienić nazwy).
Tworzenie i aktualizowanie pliku migracji
W konsoli menedżera pakietów (PMC) wprowadź następujące polecenie:
Add-Migration Inheritance
Update-Database
Uruchom polecenie w usłudze PMC. Polecenie zakończy się niepowodzeniem w tym momencie, ponieważ mamy istniejące dane, które migracje nie wiedzą, jak je obsługiwać. Zostanie wyświetlony następujący błąd:
Instrukcja ALTER TABLE powoduje konflikt z ograniczeniem KLUCZ OBCY "FK_dbo. Department_dbo. Person_PersonID". Konflikt wystąpił w bazie danych "ContosoUniversity", tabeli "dbo". Person", kolumna "PersonID".
Open Migrations< timestamp>_Inheritance.cs i zastąp metodę Up
następującym kodem:
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");
}
Uruchom ponownie polecenie update-database
.
Uwaga
Podczas migrowania danych i wprowadzania zmian schematu można uzyskać inne błędy. Jeśli wystąpią błędy migracji, których nie możesz rozwiązać, możesz kontynuować pracę z samouczkiem, zmieniając parametry połączenia w pliku Web.config lub usuwając bazę danych. Najprostszym podejściem jest zmiana nazwy bazy danych w pliku Web.config . Na przykład zmień nazwę bazy danych na CU_test, jak pokazano w poniższym przykładzie:
<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" />
W przypadku nowej bazy danych nie ma danych do zmigrowania, a update-database
polecenie jest znacznie bardziej prawdopodobne, aby ukończyć bez błędów. Aby uzyskać instrukcje dotyczące usuwania bazy danych, zobacz Jak usunąć bazę danych z programu Visual Studio 2012. Jeśli wykonasz to podejście, aby kontynuować pracę z samouczkiem, pomiń krok wdrażania na końcu tego samouczka, ponieważ wdrożona witryna będzie otrzymywać ten sam błąd podczas automatycznego uruchamiania migracji. Jeśli chcesz rozwiązać problem z błędem migracji, najlepszym zasobem jest jedno z forów programu Entity Framework lub StackOverflow.com.
Testowanie
Uruchom witrynę i wypróbuj różne strony. Wszystko działa tak samo jak wcześniej.
W Eksploratorze serwera rozwiń węzeł SchoolContext , a następnie tabele TabeleStudent i Instruktor zostały zastąpione przez tabelę Person . Rozwiń tabelę Person i zobaczysz, że zawiera ona wszystkie kolumny, które były używane w tabelach Student i Instruktor .
Kliknij prawym przyciskiem myszy tabelę Osoba, a następnie kliknij polecenie Pokaż dane tabeli , aby wyświetlić kolumnę dyskryminującą.
Na poniższym diagramie przedstawiono strukturę nowej bazy danych School:
Podsumowanie
Dziedziczenie tabeli na hierarchię zostało zaimplementowane dla Person
klas , Student
i Instructor
. Aby uzyskać więcej informacji na temat tego i innych struktur dziedziczenia, zobacz Strategie mapowania dziedziczenia na blogu Mortezy Manavi. W następnym samouczku przedstawiono kilka sposobów implementowania repozytorium i jednostek wzorców pracy.
Linki do innych zasobów programu Entity Framework można znaleźć na mapie zawartości dostępu do danych ASP.NET.