共用方式為


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

By 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 本身使用字串範本) 時,轉譯資料 (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 建立的資料,將會因為您在 方法中使用的 SeedAddOrUpdate方法而變更回其原始狀態。 (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 屬性指定為必要欄位,文字方塊的標題應該是 「Last Name」 (而不是屬性名稱,這會是沒有空格) 的 「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; }
    }
}

建置專案,以儲存您的變更,並確認您尚未進行任何複製並貼上編譯器可以攔截的錯誤。

索引鍵屬性

OfficeAssignment 實體之間 Instructor 有一對零或一的關聯性。 辦公室指派只存在於指派給它的講師上,因此其主鍵也是實體的 Instructor 外鍵。 但是 Entity Framework 無法自動辨識 InstructorID 為此實體的主鍵,因為其名稱未遵循 IDclassnameID 命名慣例。 因此,必須使用 Key 屬性將其識別為 PK:

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

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

ForeignKey 屬性

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

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

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

Instructor Navigation 屬性

實體 Instructor 具有可為 OfficeAssignment Null 的導覽屬性 (,因為講師可能沒有辦公室工作分派) ,而且 OfficeAssignment 實體具有不可 Instructor 為 Null 的導覽屬性 (,因為沒有講師的辦公室指派不存在, 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 實體具有指向相關 Department 實體的外鍵屬性 DepartmentID ,而且具有 Department 導覽屬性。 當您針對相關實體具有一個導覽屬性時,Entity Framework 便不需要您為資料模型新增一個外部索引鍵屬性。 EF 會在需要時自動在資料庫中建立外鍵。 但在資料模型中擁有外部索引鍵,可讓更新變得更為簡單和有效率。 例如,當您擷取要編輯的課程實體時,如果未載入實體,則 Department 實體為 null,因此當您更新課程實體時,必須先擷取 Department 實體。 當資料模型中包含外鍵屬性 DepartmentID 時,您不需要在更新之前擷取 Department 實體。

DatabaseGenerated 屬性

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

[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 代碼中 Column ,屬性正用來變更 SQL 資料類型對應,以便使用資料庫中的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; }
    

多對多關聯性

和 實體之間 Student 有多對多關聯性,而 Enrollment 實體會以多對多聯結資料表的形式運作,且資料庫中具有承載Course 這表示 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 到 *) , 您可以在這裡看到 1 到 0..1 之間的一對零或一對一關聯性行, (1 到 0..1 之間的) ,以及 OfficeAssignment 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 多對多關聯性,程式碼會指定聯結資料表的資料表和資料行名稱。 程式碼 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" 資料行中。

<編輯timestamp > _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 command.) 時收到錯誤

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

Seed當方法執行時,它會在資料表中 Department 插入資料列,並將現有的 Course 資料列與這些新 Department 資料列產生關聯。 如果您尚未在 UI 中新增任何課程,則不再需要 「暫存」部門或資料行的 Course.DepartmentID 預設值。 若要允許有人可能已使用應用程式新增課程的可能性,您也想要更新 Seed 方法程式碼,以確保所有 Course 資料列 (不只是先前執行方法所插入 Seed 的資料列,) 在移除資料行中的預設值並刪除 「Temp」 部門之前,先有有效的 DepartmentID 值。

編輯timestamp > _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 資源的連結。