共用方式為


在 ASP.NET MVC應用程式中實作 Entity Framework 的繼承, (8/10)

By Tom Dykstra

Contoso University 範例 Web 應用程式示範如何使用 Entity Framework 5 Code First 和 Visual Studio 2012 建立 ASP.NET MVC 4 應用程式。 如需教學課程系列的資訊,請參閱本系列的第一個教學課程

注意

如果您遇到無法解決的問題, 請下載已完成的章節 ,並嘗試重現您的問題。 一般而言,您可以將程式代碼與已完成的程式代碼進行比較,以找出問題的解決方案。 如需一些常見的錯誤以及如何解決這些問題,請參閱 錯誤和因應措施。

在上一個教學課程中,您已處理並行例外狀況。 本教學課程將示範如何在資料模型中實作繼承。

在面向物件程式設計中,您可以使用繼承來消除多餘的程序代碼。 在本教學課程中,您將變更 InstructorStudent 類別,讓它們衍生自 Person 基底類別,而此基底類別包含講師和學生通用的屬性,例如 LastName。 您不會新增或變更任何網頁,但是您將變更一些程式碼,這些變更將會自動反映在資料庫中。

數據表個別階層與數據表個別類型繼承

在面向物件程式設計中,您可以使用繼承,更輕鬆地處理相關的類別。 例如,數據模型中的 InstructorStudent 類別 School 會共享數個屬性,這會導致多餘的程式代碼:

顯示已醒目提示備援代碼的 Student 和 Instructor 課程螢幕快照。

假設您想要針對 InstructorStudent 實體所共用的屬性消除多餘的程式碼。 您可以建立 Person 只包含這些共用屬性的 Instructor 基類,然後讓 和 Student 實體繼承自該基類,如下圖所示:

顯示衍生自 Person 課程之 Student 和 Instructor 課程的螢幕快照。

有幾種方式可以在資料庫中表示此繼承結構。 您可以有一個數據表,其中包含單一 Person 表格中學生和講師的相關信息。 某些數據行只能套用至講師 () HireDate ,有些則僅適用於學生 EnrollmentDate () ,有些則同時套用至 (LastNameFirstName) 。 一般而言,您會有 一個辨別子 數據行來指出每個數據列所代表的類型。 例如,鑑別子資料行的 "Instructor" 代表講師,而 "Student" 代表學生。

顯示 Person 實體類別繼承結構的螢幕快照。

從單一資料庫數據表產生實體繼承結構的此模式稱為每個階層的 數據表 (TPH) 繼承。

替代方法是讓資料庫看起來更像繼承結構。 例如,您只能有數據表中 Person 的名稱欄位,而且具有日期欄位的個別 InstructorStudent 數據表。

顯示衍生自 Person 實體類別之新 Instructor 和 Student 資料庫數據表的螢幕快照。

針對每個實體類別建立資料庫數據表的這個模式稱為每個 類型數據表 , (TPT) 繼承。

TPH 繼承模式通常會在 Entity Framework 中提供比 TPT 繼承模式更好的效能,因為 TPT 模式可能會導致複雜的聯結查詢。 本教學課程將示範如何實作 TPH 繼承。 您將執行下列步驟來執行此動作:

  • 建立Person類別,並將和 Student 類別變更Instructor為衍生自 Person
  • 將模型對資料庫對應程式代碼新增至資料庫內容類別。
  • 將整個專案中的 與 StudentID 參考變更InstructorIDPersonID

建立 Person 類別

注意:在建立下列類別之後,您將無法在更新使用這些類別的控制器之後編譯專案。

Models 資料夾中,建立 Person.cs ,並以下列程式代碼取代範本程式代碼:

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

Instructor.csInstructor ,從類別衍生類別, Person 並移除索引鍵和名稱欄位。 程式碼看起來應該如下列範例所示:

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

Student.cs 進行類似的變更。 類別 Student 看起來會像下列範例:

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

將人員實體類型新增至模型

SchoolContext.cs 中,新增 DbSet 實體類型的屬性 Person

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

這就是 Entity Framework 為了設定單表繼承而必須執行的所有工作。 如您所見,當重新建立資料庫時,它會有數據表 Person 來取代 StudentInstructor 數據表。

將 InstructorID 和 StudentID 變更為 PersonID

SchoolContext.cs 的 Instructor-Course 對應語句中,變更 MapRightKey("InstructorID")MapRightKey("PersonID")

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

不需要這項變更;它只會變更多對多聯結數據表中 InstructorID 數據行的名稱。 如果您將名稱保留為 InstructorID,應用程式仍可正常運作。 以下是已完成的 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"));
      }
   }
}

接下來,您必須在 [轉] 資料夾中的時間戳移轉檔案中,變更InstructorIDPersonID為 和 StudentIDPersonID 至整個專案。 若要這樣做,您只會找到並開啟需要變更的檔案,然後在開啟的檔案上執行全域變更。 您應該變更的 Migrations 資料夾中唯一的檔案是 Migrations\Configuration.cs。

  1. 重要

    從關閉 Visual Studio 中所有開啟的檔案開始。

  2. 單擊 [尋找並取代] -- 在[編輯 ] 功能表中尋找所有檔案,然後搜尋包含 InstructorID的所有檔案。

    顯示 [尋找和取代] 視窗的螢幕快照。講師 I D、目前專案、比對大小寫和比對整字複選框,以及 [尋找全部] 按鈕全都會反白顯示。

  3. 在 [尋找結果] 視窗中開啟每個檔案,但 Migrations 資料夾中的 time-stamp>_.cs 移轉檔案除外<,每一個檔案按兩下一行。

    顯示 [尋找結果] 視窗的螢幕快照。時間戳移轉檔案會以紅色來交叉。

  4. 開啟 [ 檔案中的取代 ] 對話框,並將 [查看 ] 變更為 [所有開啟的檔]。

  5. 使用 [ 檔案中取代] 對話框,將所有 InstructorID 變更為 PersonID.

    顯示 [尋找和取代] 視窗的螢幕快照。[以文字取代] 欄位中輸入人員識別碼。

  6. 尋找專案中包含 StudentID的所有檔案。

  7. 在 [尋找結果] 視窗中開啟每個檔案,但 Migrations 資料夾中的 time-stamp>_*.cs 移轉檔案除外<,每一個檔案按兩下一行。

    顯示 [尋找結果] 視窗的螢幕快照。時間戳移轉檔案已逾時。

  8. 開啟 [ 檔案中的取代 ] 對話框,並將 [查看 ] 變更為 [所有開啟的檔]。

  9. 使用 [檔案 中取代] 對話框,將所有 StudentID 變更為 PersonID

    顯示 [尋找和取代] 視窗的螢幕快照。在 [檔案]、[所有開啟的檔]、[比對大小寫] 和 [比對整字] 複選框中取代 ,並醒目提示 [全部取代] 按鈕。

  10. 建置專案。

(請注意,這示範了命名主鍵模式的classnameID缺點。如果您已命名主鍵識別碼,但沒有在類別名稱前面加上前置詞,則現在不需要重新命名。)

建立和更新移轉檔案

在 [套件管理員控制台] (PMC) 中,輸入下列命令:

Add-Migration Inheritance

Update-Database在 PMC 中執行 命令。 命令此時會失敗,因為我們有移轉不知道如何處理的現有數據。 而您會收到下列錯誤:

ALTER TABLE 語句與 FOREIGN KEY 條件約束衝突:「FK_dbo」。Department_dbo。Person_PersonID」。 資料庫 「ContosoUniversity」 資料表 「dbo」 發生衝突。Person“,數據行 'PersonID'。

開啟 移轉<timestamp>_Inheritance.cs ,並以下列程序代碼取代 Up 方法:

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

再次執行 update-database 命令。

注意

移轉數據並進行架構變更時,可能會發生其他錯誤。 如果您收到無法解決的移轉錯誤,您可以變更Web.config檔案中的 連接字串 或刪除資料庫,繼續進行教學課程。 最簡單的方法是重新命名 Web.config 檔案中的資料庫。 例如,將資料庫名稱變更為 CU_test,如下列範例所示:

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

使用新的資料庫時,沒有任何數據可移轉,而且命令更可能完成,而不會 update-database 發生錯誤。 如需如何刪除資料庫的指示,請參閱 如何從Visual Studio 2012卸除資料庫。 如果您採用此方法以繼續進行本教學課程,請略過本教學課程結尾的部署步驟,因為部署的網站會在自動執行移轉時收到相同的錯誤。 如果您想要針對移轉錯誤進行疑難解答,最佳資源是其中一個 Entity Framework 論壇或 StackOverflow.com。

測試

執行網站並嘗試各種頁面。 一切項目的運作與之前一樣。

[伺服器總 管] 中,展開 [SchoolContext ] 和 [ 數據表],您會看到 StudentInstructor 數據表已由 Person 數據表取代。 展開 Person 數據表,您會看到它具有所有用於 StudentInstructor 數據表的數據行。

顯示 [伺服器總管] 視窗的螢幕快照。[數據連線]、[學校內容] 和 [數據表] 索引卷標會展開以顯示 Person 數據表。

以滑鼠右鍵按一下 Person 資料表,然後按一下 [顯示資料表資料] 以查看鑑別子資料行。

顯示 Person 數據表的螢幕快照。辨別子數據行名稱會反白顯示。

下圖說明新 School 資料庫的結構:

顯示 School 資料庫關係圖的螢幕快照。

摘要

數據表個別階層繼承現在已針對 PersonStudentInstructor 類別實作。 如需此和其他繼承結構的詳細資訊,請參閱 Morteza Manavi 部落格上的 繼承對應策略 。 在下一個教學課程中,您將會看到實作存放庫和工作模式單位的一些方式。

您可以在 ASP.NET 數據存取內容對應中找到其他 Entity Framework 資源的連結。