這是微軟官方教程Getting Started with Entity Framework 6 Code First using MVC 5 系列的翻譯,這里是第六篇:為ASP.NET MVC應用程序創建更復雜的數據模型
原文:Creating a More Complex Data Model for an ASP.NET MVC Application
譯文版權所有,謝絕全文轉載——但您可以在您的網站上添加到該教程的鏈接。
在之前的教程中您已經創建了由三個實體組成的簡單的數據模型。在本教程中,您將會添加更多的實體和關系,您會進一步定制數據模型,包括指定格式、驗證和數據庫映射規則。您會看到兩種自定義數據模型的方法:通過將屬性添加到實體類和通過將代碼添加到數據庫上下文類。
當您完成時,實體類將組成一個完整的數據模型,如下圖所示:
使用特性來定制數據模型
在本節中您會看到如何使用特性來定制數據模型的屬性來用於指定格式、驗證和數據庫映射規則。在余下的章節中您將通過向已經創建的類中添加特性及創建剩余實體的新類來完善School數據模型。
數據類型特性
對於學生注冊日期,雖然您僅僅關心該字段中的日期,但在所有Web頁面中都顯示為日期和時間。通過使用數據批注特性,您可以使用代碼來修復在每個視圖中該字段的顯示格式。為了實現這一點,您需要添加一個特性到學生類的EnrollmentDate屬性。
在Models\Student.cs中,添加System.ComponentModel.DataAnnotations命名空間的using語句,將DateType及DisplayFormat特性添加到EnrollmentDate屬性上,如下面的代碼所示:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.Models { public class Student { public int ID { get; set; } public string LastName { get; set; } public string FirstMidName { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] public DateTime EnrollmentDate { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } } }
DataType特性用於執行比數據庫內部類型更加具體的數據類型。在本示例中,我們只想保持對日期的跟蹤,而不是日期及時間。DataType枚舉提供了多種數據類型,比如日期,時間,電話號碼,電子郵件等。DataType特性同樣可以讓應用程序來自動基於數據類型的特殊功能。例如DataType.EmailAddress可以創建mailto:的超鏈接,DataType.Date特性可以在支持HTML5的瀏覽器中創建一個日期選擇器。DataType特性可以生成HTML5瀏覽器支持的HTML5 數據特性。要注意DataType特性並不提供任何驗證。
DataType.Date不指定日期的顯示格式。默認情況下, 數據字段的顯示基於服務器本身的CultureInfo的默認格式。
DisplayFormat特性用於顯示指定的日期格式:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode設置指定該值在文本框中進行編輯時也同樣適用已指定的格式(某些情況下可能並不適用,比如針對一個貨幣值,您可能不希望在文本框中顯示一個貨幣符號並對其編輯)。
您可以單獨使用DisplayFormat特性,但通常一個好注意是同時使用DataType這兩者。DataType特性所傳達的是數據本身的表述而不是如何將它呈現在屏幕上。下面列出了一些您可以考慮不使用DisplayFormat的情況:
- 目標瀏覽器可以啟用HTML5功能(比如顯示日歷控件,區域化的貨幣符號,電郵鏈接,一些客戶端輸入驗證等。)。
- 默認情況下,瀏覽器將使用基於您的區域設置的正確格式老呈現數據。
- DataType特性可以讓MVC自動選擇正確的模板來呈現數據(DisplayFormat使用字符串模板)。
如果您在日期字段上使用DataType特性,您也應當指定DisplayFormat特性以確保該字段在Chrome瀏覽器中正確呈現。詳細信息請參見StackOverflow thread。
有關如何在MVC中處理其他數據類型,請參閱中MVC 5 Introduction: Examining the Edit Methods and Edit View的國際化部分。
再次運行學生索引頁您會注意到頁面上不再顯示時間部分,所有使用學生模型的視圖都會有類似的改變。
StringLength特性
您還可以使用特新來指定數據驗證規則和驗證錯誤信息。StringLength特性設定設定數據庫的最大長度並且提供ASP.NET MVC的客戶端及服務器端驗證。您還可以在此特性中指定字符串的最小長度,但最小值對數據庫的架構沒有任何影響。
假設您想要確保用戶不能輸入超過50個字符的名稱,如果要添加該限制,將StringLength特性添加到LastName和FirstMidName屬性,如下面的示例:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.Models { public class Student { public int ID { get; set; } [StringLength(50)] public string LastName { get; set; } [StringLength(50, ErrorMessage = "名字不能超過50個字符")] 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(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
MaxLength特性提供的功能類似於StringLength,但不提供客戶端驗證。
運行程序並單擊學生選項卡,您會收到以下的異常:
“System.InvalidOperationException”類型的異常在 EntityFramework.dll 中發生,但未在用戶代碼中進行處理
其他信息: 支持“SchoolContext”上下文的模型已在數據庫創建后發生更改。請考慮使用 Code First 遷移更新數據庫(http://go.microsoft.com/fwlink/?LinkId=238269)。
實體框架會檢測到數據模型已經進行了更改並且要求數據庫架構也作出相應的改變。您將通過使用遷移來在不丟失數據的情況下升級架構。如果您更改了使用Seed方法創建的數據,您在Seed方法中所使用的AddOrUpdate方法會更改回其原始狀態(AddOrUpdate是一個相當於"upsert"操作的數據庫術語)。
在程序包管理器控制台中,輸入以下命令:
add-migration MaxLengthOnNames
update-database
add-migration命令創建一個名為<時間戳>_MaxLengthOnName.cs的文件,此文件包含用來更新數據庫的Up方法,以匹配當前數據模型中的代碼。update-database命令運行該代碼。
實體框架使用有時間戳前綴的遷移文件名來進行遷移。您可以在運行update-database命令之前創建多個遷移,所有的遷移會按照它們創建的順序來應用。
運行程序,新建一個學生,並在姓名中輸入超過50個字符,點擊創建,之后您會看到一條錯誤信息。
列特性
您還可以通過使用特性來控制如何將您的類和屬性映射到數據庫。假設您曾經使用名稱FirstMidName來作為名稱字段,因為該字段中還可能包含一個中間名。但您想讓數據庫中的列命名為FirstName,因為使用數據庫來編寫查詢的其他用戶都習慣於使用該列名。要做到這一點,您需要使用列特性。
Column特性指定在創建數據庫時,Student表映射的FirstMidName屬性的列將被命名為FirstName,換句話說,當您的代碼引用Student.FirstMidName,相應的更新等改變會在數據庫的Student表中的FirstName對應。如果您不指定列的名稱,他們會使用和屬性相同的名稱。
在Student.cs文件中,添加 System.ComponentModel.DataAnnotations.Schema的引用,並將Column特性添加到FirstMidName上,如下面的代碼所示:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { public class Student { public int ID { get; set; } [StringLength(50)] public string LastName { get; set; } [StringLength(50, ErrorMessage = "名字不能超過50個字符")] [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的模型,所以它不會匹配數據庫。在程序包管理器通知台中創建另一個遷移,輸入以下命令:
add-migration ColumnFirstName
update-database
在服務器資源管理器中,雙擊Student表,打開表格設計器。
您可以看到FirstMidName已經被命名為FirstName,而兩個列的數據最大長度都已經變更為50個字符。
您還可以使用Fluent API來更改數據庫映射,后面的教程我們將演示該做法。
注意:如果您在全部完成以下各節中創建的所有實體類之前嘗試編譯程序,您會收到編譯器錯誤。
完成對學生實體的更改
在Model\Student.cs中,使用下面的代碼替換原來的內容:
using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { public class Student { public int ID { get; set; } [Required] [Display(Name = "姓")] [StringLength(50)] public string LastName { get; set; } [Required] [StringLength(50, ErrorMessage = "名字不能超過50個字符")] [Column("FirstName")] [Display(Name = "名")] public string FirstMidName { get; set; } [Display(Name = "注冊日期")] [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] public DateTime EnrollmentDate { get; set; } [Display(Name = "全名")] public string FullName { get { return LastName + ", " + FirstMidName; } } public virtual ICollection<Enrollment> Enrollments { get; set; } } }
必需特性
Required特性使屬性成為必需的字段。值類型的字段是不需要Required特性的,比如Int,double,DateTime等,由於值類型不能分配Null值,所以它們本身就被視為必需字段。您也可以刪除Required特性並使用帶有最小長度的StringLength特性來替換它。
[Display(Name = "Last Name")] [StringLength(50, MinimumLength=1)] public string LastName { get; set; }
顯示特性
Display特性指定文本框中的標題應該是"姓","名","全名","注冊日期"而不是屬性本身的名字。
FullName計算屬性
FullName是一個計算的屬性,通過串聯其他兩個屬性來返回一個值。因此它只有get訪問器,數據庫也不會生成對應的FullName列。
創建講師實體
創建Models\Instructor.cs,使用下面的代碼替換默認生成的:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { public class Instructor { public int ID { get; set; } [Required] [Display(Name = "姓")] [StringLength(50)] public string LastName { get; set; } [Required] [Column("FirstName")] [Display(Name = "名")] [StringLength(50)] public string FirstMidName { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",ApplyFormatInEditMode = true)] [Display(Name = "聘用日期")] public DateTime HireDate { get; set; } [Display(Name="全名")] public string FullName { get { return LastName + ", " + FirstMidName; } } public virtual ICollection<Course> Courses { get; set; } public virtual OfficeAssignment OfficeAssignment { get; set; } } }
請注意講師實體和學生實體的屬性都是相同的,在后續的教程中,您會通過執行繼承來重構此代碼以消除冗余。
您也可以將多個特性放在一行上,如下面所示:
public class Instructor { public int ID { get; set; } [Display(Name = "Last Name"),StringLength(50, MinimumLength=1)] public string LastName { get; set; } [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)] public string FirstMidName { get; set; } [DataType(DataType.Date),Display(Name = "Hire Date")] public DateTime HireDate { get; set; } [Display(Name = "Full Name")] public string FullName { get { return LastName + ", " + FirstMidName; } } public virtual ICollection<Course> Courses { get; set; } public virtual OfficeAssignment OfficeAssignment { get; set; } }
Course和OfficeAssignment導航屬性
Course和OfficeAssignment是導航屬性。正如之前所解釋的,它們通常被定義為virtual,這樣它們就可以利用實體框架中的延遲加載。此外,如果一個導航屬性可以容納多個實體,則它的類型必須實現ICollection<T>接口,例如List<T>,但不能是IEnumerable<T>,因為它不實現Add。
教師可以教授任意數量的課程,所以Courses定義為Course實體的集合。
public virtual ICollection<Course> Courses { get; set; }
我們的業務邏輯定義一個講師只能有一個辦公室,因此OfficeAssignment定義為單個OfficeAssignment的實體(如果講師沒有辦公室,則可以分配Null)。
public virtual OfficeAssignment OfficeAssignment { get; set; }
創建OfficeAssignment實體
創建Models\OfficeAssignment.cs並使用下面的代碼替換自動生成的:
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { public class OfficeAssignment { [Key] [ForeignKey("Instructor")] public int InstructorID { get; set; } [StringLength(50)] [Display(Name = "辦公室地址")] public string Location { get; set; } public virtual Instructor Instructor { get; set; } } }
全部保存后生成項目,確保沒有彈出任何編譯器或可以捕捉的錯誤。
Key特性
Instructor和OfficeAssignment實體之間有一個對零或一對一的關系。辦公室只和講師之間存在關系,因此其主鍵也是其Instructor實體的外鍵。但是實體框架不會自動將InstructorID識別為實體的主鍵,因為該命名不遵循實體框架約定。因此,我們使用Key特性來標記該屬性為實體的主鍵:
[Key] [ForeignKey("Instructor")] public int InstructorID { get; set; }
如果實體沒有它自己的主鍵,但您想將屬性名命名為類名-ID或ID以外的不同的名稱,您同樣可以使用Key特性。默認情況下實體框架將鍵視為非數據庫生成的,因為該列用來標識關系。
ForeignKey特性
當兩個實體之間存在有一對零或一對一關系時,實體框架無法自動辨認出那一端的關系是主體,那一端是依賴。一對一關系在每個類中使用導航屬性來引用其他類。ForeignKey特性可以應用於要建立關系的依賴類。如果您省略ForeignKey特性,當您嘗試創建遷移時系統會出現一個無法確定實體間關系的錯誤。
Instructor導航屬性
Instructor實體有一個可為空的OfficeAssignment導航屬性(因為可能有講師沒有分配辦公室),並且OfficeAssignment實體有一個不可為空的Instuctor導航屬性(因為一個辦公室不可能在沒有講師的情況下分配出去--InstructorID是不可為空的)。當Instructor實體具有OfficeAssignment實體關聯的時候,每個實體在導航屬性中都有另一個的引用。
您可以把一個Required特性添加給Instructor導航屬性來指明必須有相關的講師,但您不需要這樣做。因為InstructorId外鍵(同樣也是表的主鍵)是不可為null的。
修改Course實體
在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 = "編號")] public int CourseID { get; set; } [StringLength(50, MinimumLength = 3)] public string Title { get; set; } [Range(0, 5)] public int Credits { get; set; } public int DepartmentID { get; set; } public virtual Department Department { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } public virtual ICollection<Instructor> Instructors { get; set; } } }
課程實體有一個DepartmentID外鍵屬性,用來指向相關的Department實體,它有一個Department導航屬性。當一個關聯實體有一個導航屬性時,實體框架不需要您添加外鍵屬性到您的實體模型,實體框架在需要時會在數據庫中自動創建外鍵。但在實體模型中擁有一個外鍵會讓更新更簡單、高效。例如,當您讀取一個Course實體進行編輯,如果您選擇不加載Department實體,那Department實體是空的。所以當您更新Course實體時,您必須先取得該實體關聯的Department實體。如果在數據模型中包含了外鍵DepartmentID,您就不需要在更新前先取得Department實體。
DatabaseGenerated特性
CourseID屬性有一個提供了None參數的DatabaseGenerated特性,該特性指明主鍵值將由用戶提供,而不是由數據庫自動生成。
[DatabaseGenerated(DatabaseGeneratedOption.None)] [Display(Name = "名稱")] 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; }
創建系實體
使用下面的代碼創建Models\Department.cs:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace ContosoUniversity.Models { public class Department { public int DepartmentID { get; set; } [StringLength(50, MinimumLength = 3)] public string Name { get; set; } [DataType(DataType.Currency)] [Column(TypeName = "money")] public decimal Budget { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "起始日期")] public DateTime StartDate { get; set; } public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; } public virtual ICollection<Course> Courses { get; set; } } }
Column特性
之前您已經使用過Column特性來更改列名稱映射。在系實體代碼中,Column特性被用於更改SQL數據類型的映射,以指明列定義將在數據庫中使用money類型。
[Column(TypeName = "money")] public decimal Budget { get; set; }
列映射通常是不必要的,因為實體框架通常會基於您定義屬性的CLR類型來自動選擇適當的SQL SERVER數據類型作為數據列的類型。CLR的decimal類型是映射到SQL Server decimal類型的,但在這種情況下,您知道該屬性將保存貨幣金額,所以指明了比decimal更適合的money數據類型來作為列的數據類型。有關CLR數據類型及它們如何匹配到SQL Server數據類型的詳細信息,請參見SqlClient for Entity FrameworkTypes。
外鍵和導航屬性
外鍵和導航屬性反映了以下關系:
- 一個系可能沒有管理員,但管理員始終是一名講師。因此,InstructorID屬性是Instructor實體的外鍵,並且在屬性類型后添加了一個問號,將其標記為可空的。導航屬性被命名為Administrator,但持有Instructor實體。
public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
- 一個系可能有很多課程,所以有一個Courses導航屬性
public virtual ICollection<Course> Courses { get; set; }
注意:基於約定,實體框架針對非空外鍵和多對多關系會啟用級聯刪除。這可能會導致循環的級聯刪除規則,使得在您嘗試添加一個遷移時導致一場。例如,如果您不將Department.InstructorID屬性定義為可為空的,您會得到一個引用關系錯誤的異常。如果您的業務規則需要InstructorID屬性設置為不可為空,則必須使用以下fluent API來聲明在關系上禁用級聯刪除。
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
修改Enrollment實體
在Models\Enrollment.cs中,使用下面的代碼替換原來的:
using System.ComponentModel.DataAnnotations; namespace ContosoUniversity.Models { public enum Grade { A, B, C, D, F } public class Enrollment { public int EnrollmentID { get; set; } public int CourseID { get; set; } public int StudentID { get; set; } [DisplayFormat(NullDisplayText = "沒有成績")] 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; }
多對多關系
在學生和課程之間有多對多的關系,並且注冊實體作為一個多對多的數據庫連接表。這意味着Enrollment數據表包含了連接表的外鍵除外的附加數據(在本例中,主鍵和Grade屬性)。
下圖顯示了在實體關系圖中這些關系的關聯情況(本圖使用實體框架Power Tools生成,創建關系圖不是本教程的一部分,在此處僅僅是做示例)。
每個關系線都有一個結束和一個型號,表明這是一個一對多的關系。
如果Enrollment數據表不包含成績信息,它只需要包含兩個外鍵CourseID和StudentID,在這種情況下,他將會在數據庫中對應無有效載荷(或純連接表)的多對多連接表,您就無需針對它們單獨創建一個模型類。Instructor和Course實體都有多對多關系,並且您可以看到,它們之間沒有實體類:
數據庫需要一個連接表,如下圖的數據庫關系圖所示:
實體框架會自動創建CourseInstructor表,並通過Instructor.Course和Course.Instructor導航屬性來間接地讀取和更新它。
在實體關系圖中顯示關系
下面的插圖顯示了使用是實體框架Power Tools創建的完整的學校模型:
除了多對多關系線(*到*)和一對多關系線(1到*),您還能看到Instructor和OfficeAssignment實體之間的一到零或1關系線(1到0..1),以及Istructor和Department實體之間的零或一對多(0..1到*)關系線。
添加代碼到數據庫上下文來自定義數據模型
下一步您將添加新實體到SchoolContext類中並使用fluent API來自定義映射。該API經常使用一系列的方法調用來合並為單個語句,如下面的示例:
modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseID") .MapRightKey("InstructorID") .ToTable("CourseInstructor"));
在本教程中,您將在不使用特性來進行的數據庫映射的部分使用fluent API,但您還可以如同使用大多數特性那樣來使用fluent API指定格式、驗證和映射規則。某些特性不能使用fluent API,例如MinimumLength,如前文所述,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方法中,我們使用了新語句來配置多對多連接表:
- 為Instructor和Course實體間配置多對多關系,該代碼指定了連接表的名稱和列名。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而不是特性來指定Instructor和OfficeAssignment實體之間的關系:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
有關fluent API的更多信息,請參閱Fluent API。
填充測試數據到數據庫中
將Migrations\Configuration.cs文件中的代碼使用下面的替換:
namespace ContosoUniversity.Migrations { using ContosoUniversity.Models; using ContosoUniversity.DAL; using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(SchoolContext context) { var students = new List<Student> { new Student { FirstMidName = "Carson", LastName = "Alexander", EnrollmentDate = DateTime.Parse("2010-09-01") }, new Student { FirstMidName = "Meredith", LastName = "Alonso", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Arturo", LastName = "Anand", EnrollmentDate = DateTime.Parse("2013-09-01") }, new Student { FirstMidName = "Gytis", LastName = "Barzdukas", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Yan", LastName = "Li", EnrollmentDate = DateTime.Parse("2012-09-01") }, new Student { FirstMidName = "Peggy", LastName = "Justice", EnrollmentDate = DateTime.Parse("2011-09-01") }, new Student { FirstMidName = "Laura", LastName = "Norman", EnrollmentDate = DateTime.Parse("2013-09-01") }, new Student { FirstMidName = "Nino", LastName = "Olivetto", EnrollmentDate = DateTime.Parse("2005-09-01") } }; students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s)); context.SaveChanges(); var instructors = new List<Instructor> { new Instructor { FirstMidName = "Kim", LastName = "Abercrombie", HireDate = DateTime.Parse("1995-03-11") }, new Instructor { FirstMidName = "Fadi", LastName = "Fakhouri", HireDate = DateTime.Parse("2002-07-06") }, new Instructor { FirstMidName = "Roger", LastName = "Harui", HireDate = DateTime.Parse("1998-07-01") }, new Instructor { FirstMidName = "Candace", LastName = "Kapoor", HireDate = DateTime.Parse("2001-01-15") }, new Instructor { FirstMidName = "Roger", LastName = "Zheng", HireDate = DateTime.Parse("2004-02-12") } }; instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s)); context.SaveChanges(); var departments = new List<Department> { new Department { Name = "English", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID }, new Department { Name = "Mathematics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID }, new Department { Name = "Engineering", Budget = 350000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = instructors.Single( i => i.LastName == "Harui").ID }, new Department { Name = "Economics", Budget = 100000, StartDate = DateTime.Parse("2007-09-01"), InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID } }; departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s)); context.SaveChanges(); var courses = new List<Course> { new Course {CourseID = 1050, Title = "Chemistry", Credits = 3, DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3, DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3, DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 1045, Title = "Calculus", Credits = 4, DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4, DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 2021, Title = "Composition", Credits = 3, DepartmentID = departments.Single( s => s.Name == "English").DepartmentID, Instructors = new List<Instructor>() }, new Course {CourseID = 2042, Title = "Literature", Credits = 4, DepartmentID = departments.Single( s => s.Name == "English").DepartmentID, Instructors = new List<Instructor>() }, }; courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s)); context.SaveChanges(); var officeAssignments = new List<OfficeAssignment> { new OfficeAssignment { InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID, Location = "Smith 17" }, new OfficeAssignment { InstructorID = instructors.Single( i => i.LastName == "Harui").ID, Location = "Gowan 27" }, new OfficeAssignment { InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID, Location = "Thompson 304" }, }; officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s)); context.SaveChanges(); AddOrUpdateInstructor(context, "Chemistry", "Kapoor"); AddOrUpdateInstructor(context, "Chemistry", "Harui"); AddOrUpdateInstructor(context, "Microeconomics", "Zheng"); AddOrUpdateInstructor(context, "Macroeconomics", "Zheng"); AddOrUpdateInstructor(context, "Calculus", "Fakhouri"); AddOrUpdateInstructor(context, "Trigonometry", "Harui"); AddOrUpdateInstructor(context, "Composition", "Abercrombie"); AddOrUpdateInstructor(context, "Literature", "Abercrombie"); context.SaveChanges(); var enrollments = new List<Enrollment> { new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID, Grade = Grade.A }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID, Grade = Grade.C }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alexander").ID, CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Alonso").ID, CourseID = courses.Single(c => c.Title == "Composition" ).CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Anand").ID, CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID }, new Enrollment { StudentID = students.Single(s => s.LastName == "Anand").ID, CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Barzdukas").ID, CourseID = courses.Single(c => c.Title == "Chemistry").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Li").ID, CourseID = courses.Single(c => c.Title == "Composition").CourseID, Grade = Grade.B }, new Enrollment { StudentID = students.Single(s => s.LastName == "Justice").ID, CourseID = courses.Single(c => c.Title == "Literature").CourseID, Grade = Grade.B } }; foreach (Enrollment e in enrollments) { var enrollmentInDataBase = context.Enrollments.Where( s => s.Student.ID == e.StudentID && s.Course.CourseID == e.CourseID).SingleOrDefault(); if (enrollmentInDataBase == null) { context.Enrollments.Add(e); } } context.SaveChanges(); } void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName) { var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle); var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName); if (inst == null) crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName)); } } }
正如您之前在第一個教程中看到的,大部分代碼只是簡單地更新或創建了新的實體對象並且讀取測試數據到屬性用於測試。但是請注意Course實體,它和Instructor實體之間存在多對多的關聯:
var courses = new List<Course> { new Course {CourseID = 1050, Title = "Chemistry", Credits = 3, DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID, Instructors = new List<Instructor>() }, ... }; courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s)); context.SaveChanges();
當創建Course對象時,Instructor導航屬性被使用代碼Instructors = new List<Instructor>()來初始化為一個空的集合。這使它能夠使用Instructors.Add方法來添加Instructor實體相關的Course實體。如果您沒有創建一個空的列表,您就不能夠進行添加,因為Instructors屬性為null,所以也不會有一個Add方法來添加這些關系。您同樣可以在構造函數中初始化該列表。
添加遷移和更新數據庫
從程序包管理器控制台中,輸入add-migration命令(先不要運行update-database命令):
add-Migration ComplexDataModel
如果您試圖再次運行update-database命令,您會收到一個外鍵沖突錯誤。
當您在保存現有數據的狀態下執行遷移時,您需要將存根數據插入到數據庫以滿足外鍵約束要求,所以我們現在就來做這些。在ComplexDataModel中的up方法生成的代碼將為Course數據表添加一個非空DepartmentID外鍵。因為針對Course數據表中的已有行執行代碼時AddColumn操作將失敗,因為SQL Server不知道使用何值來填充不可為空的列。因此,必須更改代碼,提供一個默認值給新列,並創建一個"Temp"作為默認系的存根。因此,在Up方法中使用Temp系來分配給Course的現有行。您可以在Seed方法中重新分配給它們正確的系。
編輯<時間戳>_ComplexDataModel.cs文件,注釋掉Course數據表中添加DepartmentID行的代碼,並添加以下高亮的代碼:
CreateTable( "dbo.CourseInstructor", c => new { CourseID = c.Int(nullable: false), InstructorID = c.Int(nullable: false), }) .PrimaryKey(t => new { t.CourseID, t.InstructorID }) .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true) .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true) .Index(t => t.CourseID) .Index(t => t.InstructorID); // Create a department for course to point to. Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())"); // default value for FK points to department created above. AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1)); //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false)); AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
當Seed方法運行時,它會在Department數據表中插入行並且會涉及現有的Course行到新的Department行。如果您還沒有在UI中添加任何課程,您可能之后不再需要Temp系或Course.DepartmentID行的缺省值。如果要考慮使用應用程序的人可能已經添加了課程的可能性,您也許會更新Seed方法代碼來確保在您從列中刪除默認值和Temp系之前所有Course行(不光是較早由Seed方法插入的)都具有有效的DepartmentID值。
編輯完文件后,在程序包資源管理器中輸入update-database命令。
updata-database
注意,在遷移數據並進行架構變更時您可能會得到某些錯誤,如果您不能解決遷移錯誤,您可以嘗試更改連接字符串的名稱或刪除數據庫。最簡單的方法是重命名web.config文件中的數據庫名稱來創建一個新的。
新的數據庫沒有數據需要遷移,並且updata-database命令更有可能在沒有錯誤的情況下完成。如果失敗,您可以嘗試重新初始化數據庫,通過輸入以下命令:
update-database -TargetMigration:0
在服務器資源管理器中打開數據庫,展開表節點來觀察是否所有的表都已經成功創建(如果您較早已經打開過,嘗試刷新一下)。
您並沒有針對CourseInstructor數據表創建數據模型,如前所述,這是Instructor和Course實體之間的多對多關系的連接表。
右鍵單擊CourseInstructor表,選擇顯示表數據以驗證其中的數據。
總結
您現在擁有一個更加復雜的數據模型和顯影的數據庫。在之后的教程中您會了解更多關於使用不同方式來訪問數據的方法。
作者信息
Tom Dykstra - Tom Dykstra是微軟Web平台及工具團隊的高級程序員,作家。