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:

Zrzuty ekranu pokazujące zajęcia uczniów i instruktorów z wyróżnionymi nadmiarowymi kodami.

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:

Zrzut ekranu przedstawiający klasy Student i Instruktor pochodzący z klasy Person.

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.

Zrzut ekranu przedstawiający strukturę dziedziczenia z klasy jednostki Person.

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.

Zrzut ekranu przedstawiający nowe tabele bazy danych Instruktor i Student pochodzące z klasy jednostki Person.

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 i Student , aby pochodziły z klasy Person.
  • Dodaj kod mapowania modelu do bazy danych do klasy kontekstu bazy danych.
  • Zmień InstructorID odwołania i StudentID w całym projekcie na PersonID.

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

  1. Ważne

    Zacznij od zamknięcia wszystkich otwartych plików w programie Visual Studio.

  2. Kliknij pozycję Znajdź i zamień — znajdź wszystkie pliki w menu Edycja , a następnie wyszukaj wszystkie pliki w projekcie zawierającym InstructorIDplik .

    Zrzut ekranu przedstawiający okno Znajdź i Zamień. Wyróżniono pola wyboru Instruktor I D, Current Project, Match case i Match whole word (Uwzględnij całe wyrazy) i Find All (Znajdź wszystko).

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

    Zrzut ekranu przedstawiający okno Znajdowanie wyników. Pliki migracji sygnatury czasowej są przekraczane na czerwono.

  4. Otwórz okno dialogowe Zamień w plikach i zmień pozycję Look in na All Open Documents (Wyszukiwanie we wszystkich otwartych dokumentach).

  5. Użyj okna dialogowego Zamień w plikach, aby zmienić wszystko InstructorID na PersonID.

    Zrzut ekranu przedstawiający okno Znajdź i Zamień. W polu tekstowym Zastąp element Person I D .

  6. Znajdź wszystkie pliki w projekcie, które zawierają StudentIDelement .

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

    Zrzut ekranu przedstawiający okno Znajdź wyniki. Pliki migracji sygnatury czasowej są przekraczane.

  8. Otwórz okno dialogowe Zamień w plikach i zmień pozycję Look in na All Open Documents (Wyszukiwanie we wszystkich otwartych dokumentach).

  9. Użyj okna dialogowego Zamień w plikach , aby zmienić wszystko StudentID na PersonID.

    Zrzut ekranu przedstawiający okno Znajdź i Zamień. Zastąp w polach wyboru Pliki, Wszystkie otwarte dokumenty, Wielkość dopasowania i Uwzględnij całe wyrazy i Zastąp wszystkie przyciski.

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

Zrzut ekranu przedstawiający okno Eksplorator serwera. Karty Połączenia danych, Kontekst szkoły i Tabele są rozwinięte, aby wyświetlić tabelę Person.

Kliknij prawym przyciskiem myszy tabelę Osoba, a następnie kliknij polecenie Pokaż dane tabeli , aby wyświetlić kolumnę dyskryminującą.

Zrzut ekranu przedstawiający tabelę Osoba. Wyróżniono nazwę kolumny Dyskryminator.

Na poniższym diagramie przedstawiono strukturę nowej bazy danych School:

Zrzut ekranu przedstawiający diagram bazy danych Szkoły.

Podsumowanie

Dziedziczenie tabeli na hierarchię zostało zaimplementowane dla Personklas , Studenti 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.