共用方式為


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

Tom Dykstra

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

注意

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

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

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

School_class_diagram

使用屬性自訂資料模型

在本節中,您會了解到如何使用指定格式、驗證和資料庫對應規則的屬性來自訂資料模型。 然後在下列幾個區段中,您將藉由將屬性新增至您已建立的類別,併為模型中剩餘的實體類型建立新類別,以建立完整的 School 數據模型。

DataType 屬性

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

Models\Student.cs中,新增 using 命名空間的 System.ComponentModel.DataAnnotations 語句,並將 和 DisplayFormat 屬性新增DataTypeEnrollmentDate 屬性,如下列範例所示:

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 屬性也可讓應用程式自動提供類型的特定功能。 例如,mailto:可以為 DataType.EmailAddress 建立連結,而且可以在支援 HTML5 的瀏覽器中為 DataType.Date 提供日期選取器。 DataType 屬性會發出 HTML 5 資料(發音為數據破折號)屬性,HTML 5 瀏覽器可以理解。 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

The 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 移轉 來更新資料庫 (https://go.microsoft.com/fwlink/?LinkId=238269)。

資料庫模型已以需要資料庫架構變更的方式變更,且 Entity Framework 偵測到該變更。 您將使用移轉來更新架構,而不會遺失您使用 UI 新增至資料庫的任何數據。 如果您變更了 方法Seed所建立的數據,將會因為您在 方法中使用 Seed AddOrUpdate 方法而變更回其原始狀態。 (AddOrUpdate 相當於資料庫術語中的「upsert」作業。

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

add-migration MaxLengthOnNames
update-database

此命令 add-migration MaxLengthOnNames 會建立名為 <timeStamp>_MaxLengthOnNames.cs 的檔案。 此檔案包含將更新資料庫以符合目前數據模型的程序代碼。 Entity Framework 會使用移轉檔名前面加上的時間戳來排序移轉。 建立多個移轉之後,如果您卸除資料庫,或是使用移轉部署專案,則會依照建立移轉的順序套用所有移轉。

執行 [ 建立] 頁面,並輸入長度超過 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 數據表的螢幕快照。

下圖顯示您在套用前兩個移轉之前的原始資料行名稱。 除了從 變更 FirstMidNameFirstName的數據行名稱之外,兩個名稱數據行的長度已從 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 Calculated 屬性

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

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

Courses 和 OfficeAssignment 導覽屬性

CoursesOfficeAssignment 屬性為導覽屬性。 如先前所述,它們通常會定義為 虛擬 ,以便利用稱為 延遲載入的 Entity Framework 功能。 此外,如果導覽屬性可以保存多個實體,則其類型必須實 作 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 實體的外部索引鍵。 但是 Entity Framework 無法自動辨識 InstructorID 為此實體的主鍵,因為它的名稱未遵循 IDclassname ID 命名慣例。 因此,必須使用 Key 屬性將其識別為 PK:

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

如果實體擁有自己的主鍵,但您想要將屬性命名為與 classnameIDID不同的屬性,您也可以使用 Key 屬性。 根據預設,EF 會將索引鍵視為非資料庫產生的,因為數據行是用於識別關聯性。

ForeignKey 屬性

當兩個實體之間有一對零或一個關聯性或兩個實體之間的一對一關聯性時,OfficeAssignmentInstructorEF 無法找出關聯性的結尾是主體,而哪個端相依。 一對一關聯性在每個類別中都有參考導覽屬性至另一個類別。 ForeignKey 屬性可以套用至相依類別,以建立關聯性。 如果您省略 ForeignKey 屬性,當您嘗試建立移轉時,會收到下列錯誤:

無法判斷類型 'ContosoUniversity.Models.OfficeAssignment' 和 'ContosoUniversity.Models.Instructor' 之間的關聯主體結尾。 此關聯的主要結尾必須使用關聯性 Fluent API 或數據批註明確設定。

稍後在本教學課程中,我們將示範如何使用 Fluent API 設定此關聯性。

Instructor 導覽屬性

實體 Instructor 具有可為 Null 的 OfficeAssignment 導覽屬性(因為講師可能沒有辦公室工作分派),而 OfficeAssignment 實體具有不可為 Null 的 Instructor 導覽屬性(因為沒有講師的辦公室工作分派無法存在, InstructorID 因此不可為 Null。 Instructor當實體有相關的OfficeAssignment實體時,每個實體在其導覽屬性中都會有另一個實體的參考。

您可以將屬性放在 [Required] Instructor 導覽屬性上,以指定必須有相關的講師,但您不需要這麼做,因為 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; }
   }
}

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

DatabaseGenerated 屬性

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

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

根據預設,Entity Framework 會假設主鍵值是由資料庫產生。 這是您在大多數案例下所希望的情況。 然而,針對 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 代碼中,屬性正用來變更 SQL 數據類型對應, Column 以便使用資料庫中的 SQL Server money 類型來定義數據行:

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

通常不需要數據行對應,因為 Entity Framework 通常會根據您為 屬性定義的 CLR 類型來選擇適當的 SQL Server 數據類型。 CLR decimal 類型會對應到 SQL Server 的 decimal 類型。 但在此情況下,您知道數據行會持有貨幣金額,而 money 數據類型則更適合使用。

外鍵和導覽屬性

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

  • 部門可以有或沒有一位系統管理員,而系統管理員一律為講師。 因此, InstructorID 屬性會包含為實體的外鍵 Instructor ,並在類型指定之後 int 加入問號,以將屬性標示為可為 Null。導覽屬性已命名 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;建立圖表不是本教學課程的一部分,它只是在這裡用來做為圖例。

Student-Course_many to-many_relationship

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

Enrollment 資料表並未包含成績資訊,則其便只需要包含兩個外部索引鍵:CourseIDStudentID。 在此情況下,它會對應至資料庫中沒有承載的多對多聯結數據表(或純聯結數據表),您完全不需要為其建立模型類別。 InstructorCourse 實體具有這種多對多關聯性,如您所見,它們之間沒有實體類別:

Instructor-Course_many-to-many_relationship

不過,資料庫中需要聯結數據表,如下列資料庫關係圖所示:

Instructor-Course_many-to-many_relationship_tables

Entity Framework 會自動建立CourseInstructor數據表,而且您藉由讀取和更新 和 Course.Instructors 導覽屬性來間接讀取和更新Instructor.Courses數據表。

顯示關聯性的實體圖表

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

School_data_model_diagram

除了多對多關聯線 (* 到 *) 和一對多關聯性線 (1 到 *),您還可以在這裡看到與 OfficeAssignment 實體之間的一對零或一對一關聯線 (1 到 0..1), 以及 Instructor 與 Department 實體之間的Instructor零或一對多關聯線 (0..1 到 *)。

將程式代碼新增至資料庫內容,以自定義數據模型

接下來,您會將新的實體新增至 類別, SchoolContext 並使用 Fluent 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 方法中的新語句會設定多對多聯結數據表:

  • 針對和 Course 實體之間的Instructor多對多關聯性,程式代碼會指定聯結數據表的數據表和數據行名稱。 Code First 可以為您設定沒有此程式碼的多對多關聯性,但如果您未呼叫它,您就會取得數據行的預設名稱,例如InstructorInstructorIDInstructorID

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

下列程式代碼提供一個範例,說明如何使用 Fluent API,而不是屬性來指定 和 OfficeAssignment 實體之間的Instructor關聯性:

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 = new List<Instructor>()將導覽屬性初始化Instructors為空集合。 這可讓您使用 Instructors.Add 方法新增Instructor與此相關的Course實體。 如果您未建立空白清單,您將無法新增這些關聯性,因為 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 中新增任何課程,則不再需要 「Temp」 部門或數據行的 Course.DepartmentID 預設值。 為了允許有人可能已使用應用程式新增課程的可能性,您也想要更新 Seed 方法程序代碼,以確保所有數據 Course 列(不只是先前 Seed 執行方法插入的數據列)都有有效的 DepartmentID 值,再從數據行中移除預設值並刪除 “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 模型類別。 如先前所述,這是 與 Course 實體之間Instructor多對多關聯性的聯結數據表。

在數據表上CourseInstructor按下滑鼠右鍵,然後選取 [顯示數據表數據],確認其中具有您新增至Course.Instructors導覽屬性的Instructor實體所產生的數據。

Table_data_in_CourseInstructor_table

摘要

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

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