共用方式為


為 ASP.NET MVC 應用程式建立更複雜的資料模型(4/10)

演講者:Tom Dykstra

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

注意

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

在前面的教學課程中,您使用了一個由三個實體組成的簡單資料模型。 在本教學課程中,您將新增更多實體和關係,並透過指定格式、驗證和資料庫對應規則來自訂資料模型。 您將會看到兩種方式來自定義數據模型:將屬性新增至實體類別,以及將程式代碼新增至資料庫內容類別。

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

學校班級圖

使用屬性自訂資料模型

在本節中,您會了解到如何使用指定格式、驗證和資料庫對應規則的屬性來自訂資料模型。 然後,在以下幾個部分中,您將透過向已建立的類別新增屬性並為模型中的其餘實體類型建立新類別來建立完整的 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 StudentID { 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)]
public DateTime EnrollmentDate { get; set; }

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

您可單獨使用 DisplayFormat 屬性,但通常建議一並使用 DataType 屬性。 DataType 屬性會傳逹資料的語意,而不是在螢幕上呈現資料的方式,並提供下列 DisplayFormat 不具備的優點:

  • 瀏覽器可以啟用 HTML5 功能 (例如顯示日曆控制項、地區專屬的貨幣符號、電子郵件連結等)。
  • 根據預設,瀏覽器會根據您的地區設定,使用正確的格式來呈現資料。
  • DataType 屬性可以讓 MVC 選擇正確的欄位範本來轉譯資料 (如果單獨使用,DisplayFormat 會使用字串範本)。 如需詳細資訊,請參閱 Brad Wilson 的 ASP.NET MVC 2 範本。 (雖然本文章針對 MVC 2 而撰寫,仍適用於目前的 ASP.NET MVC 版本。)

如果您使用 DataType 屬性來搭配日期欄位,也必須指定 DisplayFormat 屬性,才能確保欄位在 Chrome 瀏覽器中正確呈現。 如需更多資訊,請參閱這則 StackOverflow 討論串

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

Students_index_page_with_formatted_date

StringLengthAttribute

您也可以使用屬性來指定資料驗證規則和訊息。 假設您想要確保使用者不會在名稱中輸入超過 50 個字元。 若要新增此限制,請將 StringLength 屬性新增至 LastNameFirstMidName 屬性,如下列範例所示:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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 MaxLengthOnNames 指令會建立一個名為 <timeStamp>_MaxLengthOnNames.cs 的檔案。 此檔案包含將更新資料庫以符合目前數據模型的程序代碼。 實體框架使用遷移檔案名稱前面的時間戳來對遷移進行排序。 建立多個移轉之後,如果您卸除資料庫,或是使用移轉部署專案,則會依照建立移轉的順序套用所有移轉。

執行建立頁面,然後輸入長度超過 50 個字元的名稱。 一旦您超過 50 個字元,客戶端驗證就會立即顯示錯誤訊息。

用戶端 val 錯誤

列屬性

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

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

將 System.ComponentModel.DataAnnotations.Schema 和數據行名稱屬性的 using 語句新增至 FirstMidName 屬性,如下列醒目提示的程式代碼所示:

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

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int StudentID { 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

[伺服器總管] 中,如果您使用 Express for Web,請按兩下 Student 數據表。

顯示伺服器總管中 Student 數據表的螢幕快照。

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

顯示伺服器總管中 Student 數據表的螢幕快照。上一個螢幕快照中的 [名字] 行已變更為 [名字中名稱]。

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

注意

如果您在完成建立所有這些實體類別之前嘗試編譯,您可能會收到編譯程序錯誤。

建立 Instructor 實體

Instructor_entity

建立 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 InstructorID { 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; }

        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

請注意,當中有幾個屬性跟 StudentInstructor 實體中的一樣。 在本系列稍後的 實作繼承 教學課程中,您將重構使用繼承來消除此備援。

必要和顯示屬性

屬性上的 LastName 屬性會指定其為必要字段,文本框的標題應該是 「姓氏」(而不是屬性名稱,也就是 “LastName” 且沒有空格),而且值不能超過 50 個字元。

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

StringLength 屬性設定資料庫中的最大長度,並為 ASP.NET MVC 提供用戶端和伺服器端驗證。 您也可以在此屬性中指定最小字串長度,但最小值不會對資料庫結構描述造成任何影響。 實值型別不需要 Required 屬性,例如 DateTime、int、double 和 float。 實值型別無法指派 Null 值,因此原本就是必要的。 您可以移除 Required 屬性 ,並將它取代為屬性的 StringLength 最小長度參數:

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

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

public class Instructor
{
   public int InstructorID { 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; }

   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

FullName 運算屬性

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

public string FullName
{
    get { return LastName + ", " + FirstMidName; }
}

Courses 和 OfficeAssignment 導覽屬性

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

講師可以教授任意數量的課程,因此 Courses 被定義為 Course 實體的集合。 我們的業務規則規定,一名講師最多只能擁有一個辦公室,因此被 OfficeAssignment 定義為單一 OfficeAssignment 實體 (如果沒有分配辦公室,則可能是 null)。

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

建立 OfficeAssignment 實體

OfficeAssignment_entity

使用以下程式碼建立 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。

修改 Course 實體

Course_entity

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

      [Display(Name = "Department")]
      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_entity

使用以下程式碼建立 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)]
      public DateTime StartDate { get; set; }

      [Display(Name = "Administrator")]
      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 類型。 但在本例中,您知道該列將保存貨幣金額,且貨幣資料類型更適合於此。

外鍵和導航屬性

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

  • 部門可以有或沒有一位系統管理員,而系統管理員一律為講師。 因此,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 可為 Null,當初始化運算式執行時,您會收到下列例外狀況訊息:「引用關聯性會導致不允許的循環參考」。如果您的商務規則必要 InstructorID 屬性為不可為 Null,您必須使用下列 Fluent API 來停用關聯性上的串聯刪除:

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

修改學生實體

Student_entity

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 StudentID { get; set; }

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

      [StringLength(50, MinimumLength = 1, 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)]
      [Display(Name = "Enrollment Date")]
      public DateTime EnrollmentDate { get; set; }

      public string FullName
      {
         get { return LastName + ", " + FirstMidName; }
      }

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

註冊實體

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 是「fluent」,因為它通常藉由將一系列方法呼叫串連至單一語句來使用。

在本教學課程中,您將僅使用 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 System;
   using System.Collections.Generic;
   using System.Data.Entity;
   using System.Data.Entity.Migrations;
   using System.Linq;
   using ContosoUniversity.Models;
   using ContosoUniversity.DAL;

   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").InstructorID },
                new Department { Name = "Mathematics", Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").InstructorID },
                new Department { Name = "Engineering", Budget = 350000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Harui").InstructorID },
                new Department { Name = "Economics",   Budget = 100000, 
                    StartDate = DateTime.Parse("2007-09-01"), 
                    InstructorID  = instructors.Single( i => i.LastName == "Kapoor").InstructorID }
            };
         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").InstructorID, 
                    Location = "Smith 17" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Harui").InstructorID, 
                    Location = "Gowan 27" },
                new OfficeAssignment { 
                    InstructorID = instructors.Single( i => i.LastName == "Kapoor").InstructorID, 
                    Location = "Thompson 304" },
            };
         officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.Location, 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").StudentID, 
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, 
                    Grade = Grade.A 
                },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, 
                    Grade = Grade.C 
                 },                            
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Alexander").StudentID,
                    CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, 
                    Grade = Grade.B
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                     StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment {
                    StudentID = students.Single(s => s.LastName == "Alonso").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, 
                    Grade = Grade.B 
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Anand").StudentID,
                    CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
                    Grade = Grade.B         
                 },
                new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Barzdukas").StudentID,
                    CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Li").StudentID,
                    CourseID = courses.Single(c => c.Title == "Composition").CourseID,
                    Grade = Grade.B         
                 },
                 new Enrollment { 
                    StudentID = students.Single(s => s.LastName == "Justice").StudentID,
                    CourseID = courses.Single(c => c.Title == "Literature").CourseID,
                    Grade = Grade.B         
                 }
            };

         foreach (Enrollment e in enrollments)
         {
            var enrollmentInDataBase = context.Enrollments.Where(
                s =>
                     s.Student.StudentID == 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,
       Department = departments.Single( s => s.Name == "Engineering"),
       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 命令:

PM> add-Migration Chap4

如果您此時嘗試更新資料庫,您將會收到下列錯誤:

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

<編輯時間戳>_Chap4.cs檔案,並進行下列程式代碼變更(您將新增 SQL 語句並修改AddColumn語句):

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));
    AddForeignKey("dbo.Course", "DepartmentID", "dbo.Department", "DepartmentID", cascadeDelete: true);
    CreateIndex("dbo.Course", "DepartmentID");
}

public override void Down()
{

(請確定您在新增新行時將現有的 AddColumn 行批註或刪除,或在輸入 update-database 命令時收到錯誤。

有時候,當您使用現有數據執行移轉時,您需要將存根數據插入資料庫中,以滿足外鍵條件約束,這就是您現在要執行的動作。 產生的程式代碼會將不可為 Null 的 DepartmentID 外鍵新增至 Course 數據表。 如果程式代碼執行時數據表中已經有數據列 Course ,作業將會失敗, AddColumn 因為 SQL Server 不知道要放入資料行中不能為 Null 的值。 因此,您已變更程式碼來為新數據行提供預設值,而且您已建立名為 “Temp” 的存根部門,以作為預設部門。 因此,如果此程式代碼執行時有現有的 Course 數據列,它們全都會與 “Temp” 部門相關。

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

<編輯時間戳>_Chap4.cs檔案之後,請在 PMC 中輸入 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 中刪除資料庫

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

顯示 [伺服器總管] 資料庫的螢幕快照。[數據表] 節點已展開。

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

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

課程講師表中的資料表

摘要

您現在已有了更複雜的資料模型和對應的資料庫。 在下列教學課程中,您將深入瞭解存取相關數據的不同方式。

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