共用方式為


教學課程:為 ASP.NET MVC 應用程式建立更複雜的資料模型

在前面的教學課程中,您使用了一個由三個實體組成的簡單資料模型。 在本教學課程中,您將新增更多實體和關係,並透過指定格式、驗證和資料庫對應規則來自訂資料模型。 本文展示了兩種自訂資料模型的方法:為實體類別新增屬性和向資料庫上下文類別新增程式碼。

當您完成時,實體類別會構成如下列圖例中所顯示的完整資料模型:

學校班級圖

在本教學課程中,您已:

  • 客製化資料模型
  • 更新學生實體
  • 建立 Instructor 實體
  • 建立 OfficeAssignment 實體
  • 修改課程實體
  • 建立 Department 實體
  • 修改 Enrollment 實體
  • 將程式碼新增至資料庫上下文
  • 將測試資料植入資料庫
  • 新增移轉
  • 更新資料庫

必要條件

客製化資料模型

在本節中,您會了解到如何使用指定格式、驗證和資料庫對應規則的屬性來自訂資料模型。 然後,在以下幾個部分中,您將透過向已建立的類別新增屬性並為模型中的其餘實體類型建立新類別來建立完整的 School 資料模型。

資料類型屬性

針對學生的註冊日期,所有網頁目前都會同時顯示時間和日期,即使您針對此欄位只需要日期而已。 使用資料註解屬性,您可以透過僅對一個程式碼進行變更,來修正每個顯示資料的檢視上的顯示格式。 為了查看如何進行此操作的範例,您將會新增一個屬性至 Student 類別中的 EnrollmentDate 屬性。

Models\Student.cs 中,新增using命名空間System.ComponentModel.DataAnnotations的語句,並在DataType屬性上新增 DisplayFormatEnrollmentDate 屬性,如下例所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType 屬性用於指定比資料庫內在類型更具體的資料類型。 在此案例中,我們只想要追蹤日期,而非日期和時間。 DataType 列舉提供許多資料類型,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。 DataType 屬性也可讓應用程式自動提供類型的特定功能。 舉例來說,DataType.EmailAddress可建立 mailto: 連結,且在支援 HTML5 的瀏覽器可使用 DataType.Date 的日期選擇器。 DataType 屬性會提供 HTML 5 瀏覽器可接受的 HTML 5 data- (讀為 data dash) 屬性。 DataType 屬性不會提供任何驗證。

DataType.Date 未指定顯示日期的格式。 根據預設,資料欄位會根據以伺服器的 CultureInfo 為基礎的預設格式來顯示。

DisplayFormat 屬性用來明確指定日期格式:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

ApplyFormatInEditMode 設定會指定在要編輯的文字方塊顯示值時,也套用指定的格式。 (某些欄位可能不適合這種方法,例如貨幣值;您可能不希望在要編輯的文字方塊中出現貨幣符號。)

您可單獨使用 DisplayFormat 屬性,但通常建議一並使用 DataType 屬性。 DataType 屬性傳達資料的語義,而不是如何在螢幕上呈現資料,並提供以下您無法獲得的好處DisplayFormat

  • 瀏覽器可以啟用 HTML5 功能 (例如顯示日曆控制項、適合地區設定的貨幣符號、電子郵件連結、一些用戶端輸入驗證等等)。
  • 預設情況下,瀏覽器將根據您的區域設定使用正確的格式呈現資料。
  • DataType 屬性可以讓 MVC 選擇正確的欄位範本來呈現資料 (DisplayFormat 使用字串範本)。 有關詳細信息,請參閱 Brad Wilson 的 ASP.NET MVC 2 範本。 (雖然本文章針對 MVC 2 而撰寫,仍適用於目前的 ASP.NET MVC 版本。)

如果您使用 DataType 屬性來搭配日期欄位,也必須指定 DisplayFormat 屬性,才能確保欄位在 Chrome 瀏覽器中正確呈現。 有關更多信息,請參閱此 StackOverflow 執行緒

有關如何在 MVC 中處理其他日期格式的更多信息,請轉到 MVC 5 簡介:檢查編輯方法和編輯檢視,並在頁面中搜尋「國際化」。

再次執行學生索引頁面,您會發現不再顯示註冊日期的時間。 對於使用 Student 模型的任何檢視也是如此。

Students_index_page_with_formatted_date

StringLengthAttribute

您也可以使用屬性指定資料驗證規則和驗證錯誤訊息。 StringLength 屬性設定資料庫中的最大長度,並為 ASP.NET MVC 提供用戶端和伺服器端驗證。 您也可以在此屬性中指定最小字串長度,但最小值不會對資料庫結構描述造成任何影響。

假設您想要確保使用者不會在名稱中輸入超過 50 個字元。 若要新增此限制,請將 StringLength 屬性新增至 LastNameFirstMidName 屬性,如下列範例所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

StringLength 屬性不會阻止使用者輸入名稱中的空格。 您可以使用 RegularExpression 屬性對輸入套用限制。 例如,以下程式碼要求第一個字元為大寫,其餘字元為字母:

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

MaxLength 屬性提供與 StringLength 屬性類似的功能,但不提供用戶端驗證。

運行應用程式並點擊學生索引標籤。您將收到以下錯誤:

自從建立資料庫以來,支援「SchoolContext」上下文的模型已變更。 考慮使用 Code First Migrations 來更新資料庫 (https://go.microsoft.com/fwlink/?LinkId=238269)。

資料庫模型已發生更改,需要更改資料庫架構,並且實體框架檢測到了這一點。 您將使用遷移來更新架構,而不會遺失透過 UI 新增到資料庫的任何資料。 如果您變更了 Seed 方法建立的資料,由於您在 Seed 方法中使用了 AddOrUpdate 方法,因此將變更回其原始狀態。 (AddOrUpdate 相當於資料庫術語中的「upsert」操作。)

請在套件管理員主控台 (PMC) 中輸入下列命令:

add-migration MaxLengthOnNames
update-database

add-migration 指令會建立一個名為 <timeStamp>_MaxLengthOnNames.cs 的檔案。 此檔案包含了 Up 方法中的程式碼,可更新資料庫,使其符合目前的資料模型。 update-database 命令執行了該程式碼。

實體框架使用遷移檔案名稱前面的時間戳來對遷移進行排序。 您可以在執行 update-database 命令之前建立多個遷移,然後所有遷移都會按照建立的順序套用。

執行建立頁面,然後輸入長度超過 50 個字元的名稱。 當您按一下建立時,用戶端驗證會顯示錯誤訊息:姓氏欄位必須是最大長度為 50 的字串

列屬性

您也可以使用屬性控制您的類別和屬性對應到資料庫的方式。 假設您已針對名字欄位使用 FirstMidName 作為名稱,因為欄位中可能也會包含中間名。 但您想要將資料庫資料行命名為 FirstName,因為撰寫臨機操作查詢資料庫的使用者比較習慣該名稱。 若要進行此對應,您可以使用 Column 屬性。

Column 屬性指定當建立資料庫時,Student 資料表中對應到 FirstMidName 屬性的資料行會命名為 FirstName。 換句話說,當您的程式碼參照 Student.FirstMidName 時,資料便會來自 Student 資料表中的 FirstName 資料行或在其中更新。 如果不指定列名稱,則它們的名稱將與屬性名稱相同。

Student.cs 檔案中,新增 System.ComponentModel.DataAnnotations.Schemausing 語句,並將列名 FirstMidName 屬性新增至屬性中,如下列所反白顯示的程式碼所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]       
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

新增 Column 屬性會變更支援 SchoolContext 的模型,因此它不會與資料庫相符。 在 PMC 中輸入以下命令以建立另一個遷移:

add-migration ColumnFirstName
update-database

伺服器資源管理器中,透過雙擊 Student 表開啟 Student 表設計器。

下圖顯示了應用程式前兩次遷移之前的原始列名稱。 除了列名稱從 FirstMidName 變更為 FirstName 之外,兩個名稱列的 MAX 長度也從長度變更為 50 個字元。

顯示兩個學生表的名稱和資料類型差異的兩個螢幕截圖。

您也可以使用 Fluent API 進行資料庫映射更改,正如您將在本教學課程後面看到的那樣。

注意

若您嘗試在完成建立下列章節中所有的實體類別前進行編譯,您可能會收到編譯器錯誤。

更新學生實體

Models\Student.cs 中,將先前新增的程式碼替換為以下程式碼。 所做的變更已醒目提示。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

所需的屬性

必需屬性使名稱屬性成為必填欄位。 DateTime、int、double 和 float 等 Required attribute 值類型不需要 。 值類型不能分配空值,因此它們本質上被視為必填欄位。

Required 屬性必須搭配 MinimumLength 使用,才能強制執行 MinimumLength

[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }

MinimumLengthRequired 允許空白字元以滿足驗證。 使用 RegularExpression 屬性可以完全控製字串。

顯示屬性

Display 屬性指定了文字方塊的標題應為「名字」、「姓氏」、「全名」及「註冊日期」,而非每個執行個體中的屬性名稱 (沒有使用空格鍵分隔單字的名稱)。

FullName 運算屬性

FullName 為一個計算屬性,會傳回藉由串連兩個其他屬性而建立的值。 因此它只有一個 get 存取器,資料庫中不會產生任何 FullName 欄位。

建立 Instructor 實體

建立 Models\Instructor.cs,將範本程式碼替換為以下程式碼:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public virtual ICollection<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

請注意,當中有幾個屬性跟 StudentInstructor 實體中的一樣。 在本系列稍後的實作繼承教學課程中,您會對此程式碼進行重構以消除冗餘。

您可以將多個屬性放在一行中,因此您也可以將講師類別編寫如下:

public class Instructor
{
   public int ID { get; set; }

   [Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
   public string LastName { get; set; }

   [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
   public string FirstMidName { get; set; }

   [DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
   public DateTime HireDate { get; set; }

   [Display(Name = "Full Name")]
   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

   public virtual ICollection<Course> Courses { get; set; }
   public virtual OfficeAssignment OfficeAssignment { get; set; }
}

Courses 和 OfficeAssignment 導覽屬性

CoursesOfficeAssignment 屬性為導覽屬性。 如前面所解釋的,它們通常被定義為虛擬的,以便它們可以利用稱為延遲載入的實體框架功能。 此外,如果導航屬性可以容納多個實體,則其類型必須實作 ICollection<T> 介面。 例如,IList<>T 符合條件,但 IEnumerable<>T 不符合條件,因為它沒有實作 IEnumerable<T>Add

講師可以教授任意數量的課程,因此 Courses 被定義為 Course 實體的集合。

public virtual ICollection<Course> Courses { get; set; }

我們的業務規則規定,一名講師最多只能擁有一個辦公室,因此被 OfficeAssignment 定義為單一 OfficeAssignment 實體 (如果沒有分配辦公室,則可能是 null)。

public virtual OfficeAssignment OfficeAssignment { get; set; }

建立 OfficeAssignment 實體

使用以下程式碼建立 Models\OfficeAssignment.cs

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        [ForeignKey("Instructor")]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

建置項目,這會保存您的變更並驗證您沒有犯下編譯器可以捕獲的任何複製和貼上錯誤。

關鍵屬性

InstructorOfficeAssignment 實體之間有一對零或一的關聯性。 辦公室指派只有在有指派的講師時才會存在,因此其主索引鍵也是其繫結到 Instructor 實體的外部索引鍵。 但實體框架無法自動辨識 InstructorID 為該實體的主鍵,因為它的名稱不遵循 IDID 類別名稱命名約定。 因此,必須使用 Key 屬性將其識別為 PK:

[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }

如果實體確實有自己的主鍵,但您想要將 Key 屬性命名為不同於 classnameIDID 的名稱,則也可以使用該屬性。 預設情況下,EF 將鍵視為非資料庫產生的鍵,因為該列用於標識關係。

外鍵屬性

當兩個實體之間存在一對零或一關係或一對一關係時 (例如 OfficeAssignmentInstructor 之間),EF 無法計算出該關係的哪一端是主體,哪一端是主體依賴。 一對一關係在每個類別中都有一個到另一個類別的引用導航屬性。 可以將 ForeignKey 屬性套用到依賴類別來建立關係。 如果省略 ForeignKey 屬性,當您嘗試建立遷移時,您會收到以下錯誤:

無法確定類型「ContosoUniversity.Models.OfficeAssignment」和「ContosoUniversity.Models.Instructor」之間關聯的主體端。 此關聯的主體必須使用關係流暢 API 或資料註解進行明確設定。

在本教學課程的後面部分,您將了解如何使用 Fluent API 設定這種關係。

教師導航屬性

Instructor 該實體具有可為空的 OfficeAssignment 導航屬性 (因為講師可能沒有辦公室分配),並且 OfficeAssignment 實體具有不可為空的 Instructor 導航屬性 (因為沒有講師就不可能存在辦公室分配 - InstructorID 不可為空)。 當一個 Instructor 實體具有相關 OfficeAssignment 實體時,每個實體都會在其導航屬性中引用另一個實體。

您可以在 Instructor 導航屬性上放置 [Required] 屬性來指定必須有相關的講師,但您不必這樣做,因為 InstructorID 外金鑰(也是該表的鍵) 不可為 null。

修改課程實體

Models\Course.cs 中,將先前新增的程式碼替換為以下程式碼:

using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Course
   {
      [DatabaseGenerated(DatabaseGeneratedOption.None)]
      [Display(Name = "Number")]
      public int CourseID { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      public int DepartmentID { get; set; }

      public virtual Department Department { get; set; }
      public virtual ICollection<Enrollment> Enrollments { get; set; }
      public virtual ICollection<Instructor> Instructors { get; set; }
   }
}

課程實體具有指向相關 Department 實體的外鍵屬性 DepartmentID,並且具有 Department 導航屬性。 當您針對相關實體具有一個導覽屬性時,Entity Framework 便不需要您為資料模型新增一個外部索引鍵屬性。 EF 會在資料庫中任何需要的地方自動建立外鍵。 但在資料模型中擁有外部索引鍵,可讓更新變得更為簡單和有效率。 例如,當您取得要編輯的課程實體時,如果不載入 Department 實體,則該實體為空,因此當您更新課程實體時,必須先取得 Department 實體。 當外部索引鍵屬性 DepartmentID 包含在資料模型中時,您便不需要在更新前擷取 Department 實體。

資料庫產生的屬性

CourseID 屬性上帶有 None 參數的 DatabaseGeneerated 屬性指定主鍵值由使用者提供,而不是由資料庫產生。

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

預設情況下,實體框架假定主鍵值由資料庫產生。 這是您在大多數案例下所希望的情況。 然而,針對 Course 實體,您會使用使用者定義的課程號碼,例如讓一個部門使用 1000 系列,另一個部門則使用 2000 系列等等。

外鍵和導航屬性

Course 實體中的外部索引鍵屬性和導覽屬性反映了下列關聯性:

  • 課程會指派給一個部門,因此基於上述理由,會有一個 DepartmentID 外部索引鍵和一個 Department 導覽屬性。

    public int DepartmentID { get; set; }
    public virtual Department Department { get; set; }
    
  • 由於課程可由任何數量的學生進行註冊,因此 Enrollments 導覽屬性為一個集合:

    public virtual ICollection<Enrollment> Enrollments { get; set; }
    
  • 課程可由多個講師進行教授,因此 Instructors 導覽屬性為一個集合:

    public virtual ICollection<Instructor> Instructors { get; set; }
    

建立 Department 實體

使用以下程式碼建立 Models\Department.cs

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
   public class Department
   {
      public int DepartmentID { get; set; }

      [StringLength(50, MinimumLength=3)]
      public string Name { get; set; }

      [DataType(DataType.Currency)]
      [Column(TypeName = "money")]
      public decimal Budget { get; set; }

      [DataType(DataType.Date)]
      [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
      [Display(Name = "Start Date")]
      public DateTime StartDate { get; set; }

      public int? InstructorID { get; set; }

      public virtual Instructor Administrator { get; set; }
      public virtual ICollection<Course> Courses { get; set; }
   }
}

列屬性

之前您使用 Column 屬性來變更列名稱對應。 在 Department 實體的程式碼中,Column 屬性用於變更 SQL 資料類型映射,以便使用資料庫中的 SQL Server 貨幣類型定義該列:

[Column(TypeName="money")]
public decimal Budget { get; set; }

通常不需要列映射,因為實體框架通常會根據您為屬性定義的 CLR 類型選擇適當的 SQL Server 資料類型。 CLR decimal 類型會對應到 SQL Server 的 decimal 類型。 但在本例中,您知道該列將保存貨幣金額,且貨幣資料類型更適合於此。 有關 CLR 資料類型以及它們如何與 SQL Server 資料類型相符的詳細信息,請參閱實體框架類型的 SqlClient。

外鍵和導航屬性

外部索引鍵及導覽屬性反映了下列關聯性:

  • 部門可以有或沒有一位系統管理員,而系統管理員一律為講師。 因此,InstructorID 屬性作為 Instructor 實體的外鍵包含在內,並在 int 類型指定後新增一個問號以將 Administrator 屬性標記為可為空 Instructor

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • 一個部門可能有很多課程,因此有一個 Courses 導航屬性:

    public virtual ICollection<Course> Courses { get; set; }
    

    注意

    根據慣例,Entity Framework 會為不可為 Null 的外部索引鍵和多對多關聯性啟用串聯刪除。 這可能會導致循環串聯刪除規則,並在您嘗試新增移轉時造成例外狀況。 例如,如果您沒有將 Department.InstructorID 屬性定義為可為空,您將收到以下例外狀況訊息:「引用關係將導致不允許的循環引用。」如果您的業務規則要求 InstructorID 屬性不可為空,則必須使用下列流暢的 API 語句來停用關係上的級聯刪除:

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

修改 Enrollment 實體

Models\Enrollment.cs 中,將先前新增的程式碼替換為以下程式碼

using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public enum Grade
    {
        A, B, C, D, F
    }

    public class Enrollment
    {
        public int EnrollmentID { get; set; }
        public int CourseID { get; set; }
        public int StudentID { get; set; }
        [DisplayFormat(NullDisplayText = "No grade")]
        public Grade? Grade { get; set; }

        public virtual Course Course { get; set; }
        public virtual Student Student { get; set; }
    }
}

外鍵和導航屬性

外部索引鍵屬性及導覽屬性反映了下列關聯性:

  • 註冊記錄乃針對單一課程,因此當中包含了一個 CourseID 外部索引鍵屬性及一個 Course 導覽屬性:

    public int CourseID { get; set; }
    public virtual Course Course { get; set; }
    
  • 註冊記錄乃針對單一學生,因此當中包含了一個 StudentID 外部索引鍵屬性及一個 Student 導覽屬性:

    public int StudentID { get; set; }
    public virtual Student Student { get; set; }
    

多對多關聯性

StudentCourse 實體之間存在一個多對多關聯性,且 Enrollment 實體的功能便是多對多聯結資料表,其在資料庫中帶有承載。 這表示 Enrollment 資料表除了連接資料表的外金鑰(在本例中為主鍵和 Grade 屬性) 之外還包含其他資料。

下列圖例展示了在實體圖表中這些關聯性的樣子。 (該圖表是使用 Entity Framework Power Tools 產生的;建立該圖不是本教學課程的一部分,它只是在此處用作說明。)

學生課程_多對多_關係

每個關聯性線條都在其中一端有一個「1」,並在另外一端有一個「星號 (*)」,顯示其為一對多關聯性。

Enrollment 資料表並未包含成績資訊,則其便只需要包含兩個外部索引鍵:CourseIDStudentID。 在這種情況下,它將對應於資料庫中沒有有效負載的多對多聯接 (或純聯接表),並且您根本不必為其建立模型類。 InstructorCourse 實體具有這種多對多關係,正如您所看到的,它們之間沒有實體類別:

講師課程_多對多關係

然而,資料庫中需要有一個連接表,如下資料庫圖所示:

講師課程_多對多關係_表

實體框架會自動建立 CourseInstructor 表,您可以透過讀取和更新 Instructor.CoursesCourse.Instructors 導航屬性來間接讀取和更新它。

實體關聯圖表

下列圖例顯示了 Entity Framework Power Tools 為完成的 School 模型建立的圖表。

學校資料模型圖

除了多對多關係線 (* 到 *) 和一對多關係線 (1 到 *) 之外,您還可以在此處看到一對零或一條關係線 (1 到 0..1) InstructorOfficeAssignment 實體之間以及Instructor 和Department 實體之間的零或一對多關係線 (0..1 到*)。

將程式碼新增至資料庫上下文

接下來,您將向 SchoolContext 類別新增實體並使用流暢的 API 呼叫自訂一些映射。 該 API 是“流暢的”,因為它通常透過將一系列方法呼叫串聯到單一語句中來使用,如下例所示:

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

在本教學課程中,您將僅使用 Fluent API 進行無法使用屬性進行的資料庫對應。 然而,您也可以使用 Fluent API 指定大部分透過屬性可完成的格式、驗證及對應規則。 某些屬性 (例如 MinimumLength) 無法使用 Fluent API 來套用。 如前所述,MinimumLength 不會更改架構,它僅應用用戶端和伺服器端驗證規則

某些開發人員偏好單獨使用 Fluent API,使其實體類別保持「整潔」。若想要的話,您也可以混合使用屬性和 Fluent API,並且有一些自訂項目只能使用 Fluent API 完成。但一般來說,建議的做法是在這兩種方法中選擇其中一項,然後盡可能的一致使用該方法。

若要將新實體新增至資料模型並執行未使用屬性執行的資料庫映射,請將 DAL\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; }

      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("InstructorID")
                 .ToTable("CourseInstructor"));
      }
   }
}

OnModelCreating 方法中的新語句設定多對多連接表:

  • 對於 InstructorCourse 實體之間的多對多關係,代碼指定連接表的表名和列名。 Code First 可以在沒有此程式碼的情況下為您設定多對多關係,但如果您不呼叫它,您將獲得預設名稱,例如 InstructorID 列的 InstructorInstructorID 名稱。

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

以下程式碼提供了一個範例,說明如何使用 Fluent API 而不是屬性來指定 InstructorOfficeAssignment 實體之間的關係:

modelBuilder.Entity<Instructor>()
    .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

有關 Fluent API 語句在幕後執行的操作的信息,請參閱 Fluent API 部落格文章。

將測試資料植入資料庫

Migrations\Configuration.cs 檔案中的程式碼替換為以下程式碼,以便為您建立的新實體提供種子資料。

namespace ContosoUniversity.Migrations
{
    using ContosoUniversity.Models;
    using ContosoUniversity.DAL;
    using System;
    using System.Collections.Generic;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;
    
    internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(SchoolContext context)
        {
            var students = new List<Student>
            {
                new Student { FirstMidName = "Carson",   LastName = "Alexander", 
                    EnrollmentDate = DateTime.Parse("2010-09-01") },
                new Student { FirstMidName = "Meredith", LastName = "Alonso",    
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Arturo",   LastName = "Anand",     
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Gytis",    LastName = "Barzdukas", 
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Yan",      LastName = "Li",        
                    EnrollmentDate = DateTime.Parse("2012-09-01") },
                new Student { FirstMidName = "Peggy",    LastName = "Justice",   
                    EnrollmentDate = DateTime.Parse("2011-09-01") },
                new Student { FirstMidName = "Laura",    LastName = "Norman",    
                    EnrollmentDate = DateTime.Parse("2013-09-01") },
                new Student { FirstMidName = "Nino",     LastName = "Olivetto",  
                    EnrollmentDate = DateTime.Parse("2005-09-01") }
            };

            students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var instructors = new List<Instructor>
            {
                new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie", 
                    HireDate = DateTime.Parse("1995-03-11") },
                new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",    
                    HireDate = DateTime.Parse("2002-07-06") },
                new Instructor { FirstMidName = "Roger",   LastName = "Harui",       
                    HireDate = DateTime.Parse("1998-07-01") },
                new Instructor { FirstMidName = "Candace", LastName = "Kapoor",      
                    HireDate = DateTime.Parse("2001-01-15") },
                new Instructor { FirstMidName = "Roger",   LastName = "Zheng",      
                    HireDate = DateTime.Parse("2004-02-12") }
            };
            instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
            context.SaveChanges();

            var departments = new List<Department>
            {
                new Department { Name = "English",     Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
            };
            departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
            context.SaveChanges();

            var courses = new List<Course>
            {
                new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
                new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
                  DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
                  Instructors = new List<Instructor>() 
                },
            };
            courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
            context.SaveChanges();

            var officeAssignments = new List<OfficeAssignment>
            {
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").ID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID, 
                    Location = "Thompson 304" },
            };
            officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
            context.SaveChanges();

            AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
            AddOrUpdateInstructor(context, "Chemistry", "Harui");
            AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
            AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");

            AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
            AddOrUpdateInstructor(context, "Trigonometry", "Harui");
            AddOrUpdateInstructor(context, "Composition", "Abercrombie");
            AddOrUpdateInstructor(context, "Literature", "Abercrombie");

            context.SaveChanges();

            var enrollments = new List<Enrollment>
            {
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").ID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").ID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").ID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").ID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").ID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

            foreach (Enrollment e in enrollments)
            {
                var enrollmentInDataBase = context.Enrollments.Where(
                    s =>
                         s.Student.ID == e.StudentID &&
                         s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollments.Add(e);
                }
            }
            context.SaveChanges();
        }

        void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
        {
            var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
            var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
            if (inst == null)
                crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
        }
    }
}

正如您在第一個教學課程中看到的,大部分程式碼只是更新或建立新的實體物件,並根據測試需要將範例資料載入到屬性中。 但是,請注意與 Course 實體具有多對多關係的 Instructor 實體是如何處理的:

var courses = new List<Course>
{
    new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
      DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
      Instructors = new List<Instructor>() 
    },
    ...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();

建立 Course 物件時,可以使用程式碼將 Instructors 導航屬性初始化為空集合 Instructors = new List<Instructor>()。 這使得可以使用 Instructors.Add 方法來添加與此 Course 相關的 Instructor 實體。 如果您沒有建立空列表,則將無法新增這些關係,因為 Instructors 屬性將為 null 並且沒有 Add 方法。 您也可以將清單初始化新增到建構函式中。

新增移轉

從 PMC 輸入 add-migration 指令 (暫不執行 update-database 指令):

add-Migration ComplexDataModel

若您嘗試在這個時間點執行 update-database 命令,您會接收到下列錯誤:

ALTER TABLE 陳述式與 FOREIGN KEY 條件約束 "FK_dbo.Course_dbo.Department_DepartmentID" 發生衝突。 衝突發生在 "ContoseUniversity" 資料庫、"dbo.Department" 資料表、"DepartmentID" 資料行中。

有時,當您使用現有資料執行遷移時,您需要將存根資料插入資料庫以滿足外鍵約束,而這就是您現在必須要做的。 ComplexDataModel Up 方法中產生的程式碼會為Course表格新增一個不可為 null 的DepartmentID外鍵。 因為當程式碼執行時間 Course 表中已經存在行,所以 AddColumn 操作將會失敗,因為 SQL Server 不知道要在不能為空的列中放入什麼值。 因此必須更改程式碼以為新欄位提供預設值,並建立一個名為「Temp」的存根部門作為預設部門。 因此,該 Up 方法運行後,現有 Course 行將全部與「Temp」部門相關。 您可以將它們與 Seed 方法中的正確部門相關聯。

編輯 <timestamp>_ComplexDataModel.cs 文件,註解掉將DepartmentID列新增至Course表的程式碼行,並新增以下反白的程式碼 (註解行也反白):

CreateTable(
        "dbo.CourseInstructor",
        c => new
            {
                CourseID = c.Int(nullable: false),
                InstructorID = c.Int(nullable: false),
            })
        .PrimaryKey(t => new { t.CourseID, t.InstructorID })
        .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
        .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
        .Index(t => t.CourseID)
        .Index(t => t.InstructorID);

    // Create  a department for course to point to.
    Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
    //  default value for FK points to department created above.
    AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); 
    //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));

    AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

Seed 方法運行時,它將在 Department 表中插入行,並將現有 Course 行與這些新 Department 行相關聯。 如果您尚未在 UI 中新增任何課程,則您將不再需要「臨時」部門或 Course.DepartmentID 列上的預設值。 為了考慮到有人可能透過使用該應用程式添加了課程,您還需要更新方法程式碼,以確保在刪除之前所有行 (而不僅僅是 Seed 方法早期運行插入的 Course 行) 都具有 SeedDepartmentID 有效值從列中刪除預設值並刪除「Temp」部門。

更新資料庫

編輯完 <timestamp>_ComplexDataModel.cs 檔案後,在PMC中輸入 update-database 指令來執行遷移。

update-database

注意

遷移資料和進行架構變更時可能會出現其他錯誤。 如果收到無法解決的移轉錯誤,您可以變更連接字串中的資料庫名稱,或刪除該資料庫。 最簡單的方法是在 Web.config 檔案中重新命名資料庫。 以下範例顯示名稱變更為 CU_Test:

<add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;" 
      providerName="System.Data.SqlClient" />

使用新資料庫時,無需遷移資料,且 update-database 命令更有可能順利完成。 有關如何刪除資料庫的說明,請參閱如何從 Visual Studio 2012 中刪除資料庫

如果失敗,您可以嘗試的另一件事是透過在 PMC 中輸入以下命令來重新初始化資料庫:

update-database -TargetMigration:0

像之前一樣在伺服器資源管理器中開啟資料庫,然後展開「表」節點以查看所有均已建立。 (如果您之前仍開啟伺服器資源管理器,請按一下重新整理按鈕。)

顯示伺服器資源管理器視窗的螢幕截圖。School Context 下的 Tables 資料夾已開啟。

您沒有為 CourseInstructor 表格建立模型類別。 如前所述,這是 InstructorCourse 實體之間多對多關係的連接表。

右鍵點擊 CourseInstructor 表並選擇顯示表格資料以驗證其中是否包含由於新增至 Course.Instructors 導航屬性的 Instructor 實體而產生的資料。

課程講師表中的資料表

取得程式碼

下載已完成的項目

其他資源

可以在 ASP.NET 資料存取 - 推薦資源中找到其他實體框架資源的連結。

下一步

在本教學課程中,您已:

  • 客製化資料模型
  • 更新的學生實體
  • 建立 Instructor 實體
  • 建立 OfficeAssignment 實體
  • 修改了課程實體
  • 建立部門實體
  • 修改 Enrollment 實體
  • 將程式碼新增至資料庫上下文
  • 將測試資料植入資料庫
  • 新增移轉
  • 更新資料庫

繼續閱讀下一篇文章,了解如何讀取和顯示實體框架載入到導覽屬性中的相關資料。