Contoso 大學示例 Web 應(yīng)用程序演示如何使用實體框架(EF)Core 2.0 和 Visual Studio 2017 創(chuàng)建 ASP.NET Core 2.0 MVC Web 應(yīng)用程序。 如欲了解更多本教程相關(guān)信息,請參閱 一、入門
在前面的教程中,您使用由三個實體組成的簡單數(shù)據(jù)模型。 在本章中, 您將添加多個實體和關(guān)系,并將通過指定格式化、驗證以及數(shù)據(jù)庫映射規(guī)則來自定義數(shù)據(jù)模型。
完成時,實體類構(gòu)成的完整數(shù)據(jù)模型如下圖所示:
使用特性自定義數(shù)據(jù)模型
在本節(jié)中,你將了解如何通過使用指定格式設(shè)置,驗證和數(shù)據(jù)庫的映射規(guī)則的屬性來自定義數(shù)據(jù)模型。 然后在接下來的幾節(jié)中,通過添加特性到類中,以及添加新的類來完成完整的 School 數(shù)據(jù)模型。
DataType
特性(attribute)
對于學生注冊日期,目前所有的網(wǎng)頁同時顯示日期和時間,雖然您關(guān)注的只是日期。通過使用數(shù)據(jù)注解特性,您可以只在一個地方進行代碼更改,然后所有顯示注冊日期的格式將得到修正。 為了演示如何達成此目的,您將在 Student
類的 EnrollementDate
屬性上添加一個 Attribute
(特性)。
在 Models/Student.cs
中,添加 System.ComponentModel.DataAnnotations
命名空間的引用,并在 EnrollmentDate
屬性上添加 DataType
和 DisplayFormat
特性,如下代碼所示:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
public string LastName { get; set; }
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
DataType
特性用于指定更為準確的數(shù)據(jù)庫類型。在這個例子中,我們只想跟蹤日期,而不是日期和時間。 DataType 枚舉中包含許多數(shù)據(jù)類型,例如 Date, Time, PhoneNumber, Currency, EmailAddress, 等等。應(yīng)用程序還可通過 DataType 特性自動提供類型特定的功能。例如,可以為 DataType.EmailAddress 創(chuàng)建 mailto: 鏈接,并且可以在支持 HTML5 的瀏覽器中為 DataType.Date 提供日期選擇器。 DataType 特性生成 HTML5 瀏覽器可以理解的 HTML 5 data- (發(fā)音為 data dash) 。 DataType 特性不提供任何驗證。
DataType.Date 不指定顯示日期的格式。 默認情況下,數(shù)據(jù)字段基于服務(wù)器 CultureInfo 的默認格式顯示。
DisplayFormat 特性用于顯式指定日期格式:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode
設(shè)置在文本框編輯模式下也應(yīng)用數(shù)據(jù)格式。(有時候您可能不愿意在某些字段上實現(xiàn) -- 例如,對于貨幣值,您可能不想在文本框編輯模式下出現(xiàn)貨幣符號。)
您可以單獨使用 DisplayFormat
特性, 但通常情況下,同時使用 DataType
比較好。 DataType
特性傳達的是數(shù)據(jù)的語義而不是如何在屏幕上顯示,并提供了您無法從使用 Displayformat
中得到的好處:
- 瀏覽器可以啟用 HTML5 功能 (例如顯示一個日歷控件、 區(qū)域設(shè)置對應(yīng)的貨幣符號、 電子郵件鏈接,某些客戶端輸入驗證,等等。)。
- 默認情況下,瀏覽器將根據(jù)區(qū)域設(shè)置采用正確的格式呈現(xiàn)數(shù)據(jù)。
有關(guān)詳細信息,請參閱 <input> tag helper documentation 。
運行應(yīng)用程序,轉(zhuǎn)到 Student Index 頁,可以看到,Enrollment Date 字段不再顯示時間。 其他使用 Student 模型的視圖也一樣。
image.png
StringLength
特性
您還可以通過使用特性來指定數(shù)據(jù)驗證規(guī)則及驗證錯誤消息。 StringLength
特性設(shè)置在數(shù)據(jù)庫中保存的最大長度,并為 ASP.NET MVC 應(yīng)用提供客戶端和服務(wù)端驗證。 您還可以在這個特性中指定最小字符串長度,但最小值對數(shù)據(jù)庫結(jié)構(gòu)沒有任何影響。
現(xiàn)在假設(shè)您想確保用戶不能在名字中輸入超過50個字符。 為了添加此限制, 在 LastName
和 FirstMidName
屬性上添加 StringLength
特性,如下所示:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
<span style="background-color: #FF0;">[StringLength(50)]</span>
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 ICollection<Enrollment> Enrollments { get; set; }
}
}
StringLength
特性并無法阻止用戶在名字中輸入空格??梢允褂?RegularExpress
(正則表達式)特性來限制輸入。例如,下面的代碼要求第一個字符是大寫且其余的字符都是字母:
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
MaxLength
特性提供了類似于 StringLength
特性的功能,但也沒有提供客戶端驗證功能。
現(xiàn)在,數(shù)據(jù)模型已更改,并需要將此更改反映到數(shù)據(jù)庫結(jié)構(gòu)中。 您將使用遷移來更新數(shù)據(jù)庫結(jié)構(gòu),同時不會丟失在使用應(yīng)用程序過程中已經(jīng)輸入數(shù)據(jù)庫的數(shù)據(jù)。
保存所做的更改并生成項目。 然后在項目文件夾中打開命令窗口并輸入以下命令:
dotnet ef migrations add MaxLengthOnNames
dotnet ef database update
migrations add
命令警告說可能發(fā)生數(shù)據(jù)丟失,因為更改導(dǎo)致兩個數(shù)據(jù)列的最大長度變短。 遷移功能創(chuàng)建了一個名為 <時間戳>_MaxLengthOnNames.cs
的文件。 此文件中的 Up
方法將會更新數(shù)據(jù)庫結(jié)構(gòu),以匹配當前數(shù)據(jù)模型。 database update
命令負責執(zhí)行此代碼。
Entity Framework 使用遷移文件名前的時間戳對遷移進行排序。 你可以在運行 update-database 命令前, 創(chuàng)建多個遷移,這樣所有的遷移將按照創(chuàng)建順序依次應(yīng)用。
運行應(yīng)用,轉(zhuǎn)至 Student 頁面,點擊 Create New , 并在 First Name 、Last Name 中輸入超過 50 個字符。 當你單擊創(chuàng)建,客戶端驗證顯示一條錯誤消息。
Column
特性
特性還可用于控制如何類和屬性映射到數(shù)據(jù)庫。假設(shè)你已將 FirstMidName 用于 first-name 字段(因為字段中還包含 middle name)。但同時你又希望數(shù)據(jù)庫中的列名為 FirstName, 因為編寫針對數(shù)據(jù)庫查詢的用戶習慣于該名稱。 使用 Column
特性可以達成此映射要求。
Column
特性指定當創(chuàng)建數(shù)據(jù)庫時,映射到 Student表的 FirstMidName 屬性將被命名為 FirstName。 換句話說, 當你的代碼引用 Student.FirstMidName時, 數(shù)據(jù)讀和寫都是發(fā)生在 Student 表的 FirstName 列。 如果未指定列名稱,系統(tǒng)使用屬性名作為列名。
在 Student.cs 文件中, 添加 System.ComponentModel.DataAnnotations.Schema 命名空間引用,并在 FirstMidName 屬性添加 column
特性,如下代碼中高亮所示:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[StringLength(50)]
public string LastName { get; set; }
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Column
特性的添加改變了 SchoolContext 的模型,導(dǎo)致與數(shù)據(jù)庫不再匹配。
保存所做的更改并生成項目。 然后在項目文件夾中打開命令窗口并輸入以下命令以創(chuàng)建另一個遷移:
dotnet ef migrations add ColumnFirstName
dotnet ef database update
在 SQL Server 對象資源管理器,通過雙擊 Student 表打開 Student 表設(shè)計視圖。
應(yīng)用前兩個遷移之前,F(xiàn)irstName 和 LastName 列為 nvarchar (max) 類型。 而現(xiàn)在是 nvarchar(50) 類型,并且相應(yīng)的列從 FirstMidName 更改為 FirstName。
備注
如果您完成下列部分中創(chuàng)建的所有實體類之前嘗試編譯,可能會收到編譯器錯誤。
最終的 Student 實體更改
在 Models/Student.cs 文件中,替換早期代碼為如下代碼。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
<span style="background-color: #FF0;">[Required]</span>
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
<span style="background-color: #FF0;">[Required]</span>
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
<span style="background-color: #FF0;">[Display(Name = "First Name")]</span>
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
<span style="background-color: #FF0;">[Display(Name = "Enrollment Date")]</span>
public DateTime EnrollmentDate { get; set; }
<span style="background-color: #FF0;">[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}</span>
public ICollection<Enrollment> Enrollments { get; set; }
}
}
Required
特性
Required
特性使得名字屬性成為必填字段。 無需為不可為空類型指定 Required
特性,如值類型(Datetime, int, double, float 等。)。 不可為空類型自動被視為必填字段。
您也可以移除 Require
特性,并代之以 StringLenght
特性中的一個最小長度參數(shù)。
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
Display
特性
Display
特性指定文本框的標題應(yīng)為 "First Name", "Last Name", "Full Name" 及 "Enrollment Date" 而不是使用屬性名。(屬性名單詞中沒有空格)。
FullName 計算屬性
FullName 是一個計算屬性, 由兩個其他屬性合并返回一個值。 因此它僅有一個 get 訪問器,數(shù)據(jù)庫中不會有 FullName 列生成。
創(chuàng)建 Instructor 實體
創(chuàng)建Models/Instructor.cs,并替換為以下代碼:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Hire Date")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public ICollection<CourseAssignment> CourseAssignments { get; set; }
public OfficeAssignment OfficeAssignment { get; set; }
}
}
注意到在 Student 和 Instructor 實體中有多個相同的屬性。 在本系列教程的 [Implementing Inheritance] 中,您將重構(gòu)此代碼以消除冗余。
多個特性可以放在一行上, 因此你可以像下面這樣寫 HireDate 的特性。
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
CourseAssignments 和 OfficeAssignment 導(dǎo)航屬性
CourseAssignments
和 OfficeAssignment
屬性是導(dǎo)航屬性。
一個 Instructor (教師)可以教授任意數(shù)量的 Course (課程),因此 CourseAssignments
定義為集合。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
如果導(dǎo)航屬性可以包含多個實體,其類型必須是一個可以添加、刪除和更新的實體列表。 您可以指定為 ICollection<T> 、List<T> 或者 HashSet<T> 類型。 如果您指定為 ICollection<T> 類型, EF 默認創(chuàng)建為一個 HashSet<T> 集合類型。
關(guān)于為何這是 CouseAssignment 實體集合,將在下面的多對多關(guān)系一節(jié)中詳細闡述。
Contoso 大學的業(yè)務(wù)規(guī)則指出,一個 Instructor (教師)只能有最多一個 office (辦公室),因此 OfficeAssignment 屬性只包含一個 OfficeAssignment 實體 (在未分配辦公室的情況下也可能為空)。
public OfficeAssignment OfficeAssignment { get; set; }
創(chuàng)建 OfficeAssignment 實體
創(chuàng)建 Models/OfficeAssignment.cs 替換為以下代碼:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
Key
特性
在 Instructor 和 OfficeAssignment 實體間有一個 “一 對 零或一” 的關(guān)系。 一個 OfficeAssignment 實體只與分配給的 Instructor 有關(guān)系,因此其主鍵也同時是連接到 Instructor 實體的外鍵。 但是, Entity Framework 無法自動識別 InstructorID 作為此實體的主鍵,因為其名稱未遵循 ID 或 <類名>ID 的命名約定。 因此,我們使用 Key
特性來標識 InstructorID 作為主鍵。
[Key]
public int InstructorID { get; set; }
如果實體已經(jīng)有主鍵,但你想要用不同于 <類名>ID 或 ID 的屬性名的時候,你也可以使用 Key
特性。
默認情況下, EF 將 Key
處理為 non-database-generated (非數(shù)據(jù)庫生成),因為此列是用于識別關(guān)系。
Instructor
導(dǎo)航屬性
Instructor
實體有一個可為空的 OfficeAssignment 導(dǎo)航屬性(因為教師可能沒有分配一個辦公室), OfficeAssignment
實體有一個不可為空的 Instructor
導(dǎo)航屬性(當沒有教師的時候,一個 OfficeAssignment - 辦公室分配不可能存在 -- InstructorID 不可為空)。 當 Instructor 實體具有相關(guān)的 OfficeAssignment 實體時,兩個實體都有一個指向?qū)Ψ降膶?dǎo)航屬性。
您可以在 Instructor
導(dǎo)航屬性前加一個 Required
特性以指定必須有相關(guān)的 Instructor , 但實際無需這樣做,因為 InstructorID 外鍵 (同時也是此表的主鍵)本就是不可為空的。
修改 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 = "Number")]
public int CourseID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Title { get; set; }
[Range(0, 5)]
public int Credits { get; set; }
public int DepartmentID { get; set; }
public Department Department { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
public ICollection<CourseAssignment> CourseAssignments { get; set; }
}
}
Course
實體有一個 DepartmentID 外鍵屬性和一個 Department 導(dǎo)航屬性用于指向相關(guān)的 Department 實體。
當您已經(jīng)擁有相關(guān)實體的導(dǎo)航屬性時, Entity Framework 不強求你一定要添加一個外鍵屬性到數(shù)據(jù)模型中。 EF 可以在需要的時候自動在數(shù)據(jù)庫中創(chuàng)建外鍵及外鍵的影子屬性。 但是,數(shù)據(jù)模型中具有外鍵可以使更新更簡單、 更高效。例如,當您提取一個 Course 實體用于編輯時, 如果你沒有聲明加載的話,Department 實體是空的,當您要更新 Course 實體時,您將需要先讀取 Department 實體。 如果外鍵屬性 DepartmentID 包含在數(shù)據(jù)模型中,則您無需在更新前讀取 Department 實體。
DatabaseGenerated
特性
在 CourseID 屬性上標注的 DatabaseGenerated
特性與 None
參數(shù)指定主鍵值有用戶而不是數(shù)據(jù)庫生成。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
默認情況下,實體框架假定主鍵值都由數(shù)據(jù)庫生成。 這是大部分情況下您希望的。但是,對于 Course 實體, 您將使用一個用戶指定的課程號碼,比如 1000 系列用于一個部門,2000 系列用于另外一個部門,依此類推。
DatabaseGenerated
特性也可用于生成默認值, 比如在數(shù)據(jù)庫中用于記錄數(shù)據(jù)行創(chuàng)建或更新日期的列。有關(guān)詳細信息,請參閱 Generated Properties 。
外鍵和導(dǎo)航屬性
Course 實體中的外鍵屬性和導(dǎo)航屬性反映了以下關(guān)系:
一個課程將分配到一個部門,如上面提到的一樣,這兒有一個 DepartmentID 外鍵和一個 Department 導(dǎo)航屬性。
public int DepartmentID { get; set; }
public Department Department { get; set; }
一個課程可以有任意多的學生注冊, 因此 Enrollments 導(dǎo)航屬性是個集合:
public ICollection<Enrollment> Enrollments { get; set; }
一個課程可能由多個教師執(zhí)教,因此 CourseAssignments 導(dǎo)航屬性是個集合(CourseAssignment 類型稍后解釋)。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
創(chuàng)建 Department 實體
創(chuàng)建 Models/Department.cs 并替換為以下代碼:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Column
特性
早前您使用 Column 特性更改列名映射。在 Department 實體代碼中, Column 特性用于更改 SQL 數(shù)據(jù)類型映射,以便此列在數(shù)據(jù)庫中使用 SQL Server 的 money 類型。
[Column(TypeName="money")]
public decimal Budget { get; set; }
列映射通常并無必要, 因為 Entity Framework 會基于您給屬性定義的 CLR 類型,為您選擇適合的 SQL Server 數(shù)據(jù)類型。 CLR decimal 類型映射到 SQL Server 的 deccimal 類型。 不過,本例中,您指定此列將用于保存貨幣金額,money 數(shù)據(jù)類型更為合適。
外鍵和導(dǎo)航屬性
外鍵和導(dǎo)航屬性反映了以下關(guān)系:
一個部門可以有或者沒有管理員,一個管理員總是一個教師。因此 InstructorID 屬性用于指向 Instructor 實體的外鍵,int
后面的 ?
表示這個屬性是可為空屬性。 導(dǎo)航屬性命名為 Administrator 但實際上包含的是 Instructor 實體。
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
一個部門可能有多個課程,因此有一個 Course 導(dǎo)航屬性:
public ICollection<Course> Courses { get; set; }
備注
按照約定,Entity Framework 對不為空外鍵和多對多關(guān)系實施級聯(lián)刪除。 這可能導(dǎo)致循環(huán)的級聯(lián)刪除規(guī)則,當您添加一個遷移時,會引發(fā)異常。 例如,如果您定義 Department.InstructorID 屬性可為空, EF 會配置一個級聯(lián)刪除規(guī)則,用于刪除部門, 而這并不是您所希望發(fā)生的。 如果您的業(yè)務(wù)規(guī)則要求 InstructorID 屬性不可為空,那么您必須使用如下的 fluent API 語句來禁用關(guān)系上的級聯(lián)刪除規(guī)則。
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
修改 Enrollment 實體
在 Models/Enrollment.cs 文件中,使用以下代碼替換之前的代碼:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
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 Course Course { get; set; }
public Student Student { get; set; }
}
}
外鍵和導(dǎo)航屬性
外鍵屬性和導(dǎo)航屬性反映了以下關(guān)系:
一個注冊記錄只對應(yīng)一個課程, 因此這兒有一個 CourseID 外鍵屬性和一個 Course 導(dǎo)航屬性。
public int CourseID { get; set; }
public Course Course { get; set; }
一個注冊記錄只對應(yīng)一個學生, 因此這兒有一個 StudentID 外鍵屬性和一個 Student 導(dǎo)航屬性。
public int StudentID { get; set; }
public Student Student { get; set; }
多對多關(guān)系
在 Student 和 Course 實體間,存在著一個多對多的關(guān)系, Enrollment 實體作為一個多對多的關(guān)聯(lián)表在數(shù)據(jù)庫中承載關(guān)系及相關(guān)信息。承載相關(guān)信息意味著 Enrollment 數(shù)據(jù)表除了包含關(guān)聯(lián)表的外鍵之外,還包含其他數(shù)據(jù)。(在這里,包含一個主鍵和一個年級屬性)
下圖在一個實體關(guān)系圖表中展示這些關(guān)系。(這個關(guān)系圖表使用 Entity Framework Power Tools for EF 6.x 生成; 創(chuàng)建關(guān)系圖表不在本教程范圍內(nèi),此處關(guān)系圖表僅用于展示用途。)
每一條關(guān)系連線都有一端顯示
1
和另外一端顯示 *
,以表明這是一個一對多的關(guān)系。假設(shè) Enrollment 表不包括年級信息,只需要包含兩個外鍵 CourseID 和 StudentID 。 在這種情況下, 它將是一個沒有有效負載的多對多關(guān)聯(lián)表(或者說是一個純粹的關(guān)聯(lián)表)。 Instructor 和 Course 實體就具有這種多對多關(guān)系, 您下一步就將創(chuàng)建一個沒有有效負載的實體類充當關(guān)聯(lián)表。
(EF 6.x 支持隱式多對多關(guān)聯(lián)表,但 EF Core 不支持。 有關(guān)詳細信息,請參閱 discussion in the EF Core GitHub repository 。)
CourseAssignment
實體
使用如下代碼創(chuàng)建 Models/CourseAssignment.cs 文件:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class CourseAssignment
{
public int InstructorID { get; set; }
public int CourseID { get; set; }
public Instructor Instructor { get; set; }
public Course Course { get; set; }
}
}
關(guān)聯(lián)實體名稱
在數(shù)據(jù)庫中需要有一個關(guān)聯(lián)表用于 Instructor <-> Courses 的多對多關(guān)系, 并且以實體集合方式體現(xiàn)。 常規(guī)做法是命名一個關(guān)聯(lián)實體 EntityName1EntityName2, 在這里就是 CourseInstructor 。 但是, 我們建議您選擇一個可以描述這種關(guān)系的名稱。 數(shù)據(jù)模型一開始總是簡單的,然后逐步增長,從不需要有效負載的純關(guān)聯(lián)表到需要有效負載。理想狀態(tài)下,關(guān)聯(lián)實體將在業(yè)務(wù)領(lǐng)域中擁有其自然名稱。例如,書籍(Books)和客戶(Customers)之間可以通過評分(Ratings)進行關(guān)聯(lián)。 對于當前這個關(guān)系, CourseAssignment 是一個比 CourseInstructor 更好的選擇。
復(fù)合鍵
由于外鍵不可為空,并且唯一的標識每一個數(shù)據(jù)行,這兒無需一個另外的主鍵。 InstructorID 和 CourseID 屬性可以充當復(fù)合主鍵。 在 EF 中標識復(fù)合主鍵的唯一方式是使用 Fluent API 方式 (無法通過特性標注實現(xiàn))。在下一節(jié)中您將看到如何配置復(fù)合主鍵。
復(fù)合主鍵確保了當你有多個行對應(yīng)一個課程,以及多個行對應(yīng)一個教師,同時又可以避免出現(xiàn)多個行對應(yīng)同一個教師同一個課程的情況出現(xiàn)。 Enrollment 關(guān)聯(lián)實體定義了自身的主鍵, 因此可能會出現(xiàn)這種重復(fù)項。 若要防止此類重復(fù)的存在,您可以在外鍵字段上添加唯一索引,或者配置 Enrollment 使用類似于 CourseAssignment 的復(fù)合主鍵。 有關(guān)詳細信息,請參閱 Index (索引) 。
更新數(shù)據(jù)庫上下文
在 Data/SchoolContext.cs 文件中, 添加如下高亮顯示的代碼:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
<span style="background-color: #FF0;">
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });</span>
}
}
}
這些代碼添加了新的實體,并配置 CourseAssignment 實體的復(fù)合主鍵。
使用 Fluent API 代替 attributes (特性)
DbContext 類中的 OnModelCreating 方法使用 fluent API 配置 EF 的行為。 之所以將這些 API 稱為 fluent(流暢的),因為通常用于將多個方法連接成為一條語句,比如下面這個來自 EF Core documentation 的例子:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
在本教程中, 僅為不能使用特性標注的數(shù)據(jù)庫映射使用 fluent API 。然而, 您也可以用 fluent API 指定大部分您可以使用特性實現(xiàn)的格式,驗證和映射規(guī)則。 某些特性,如 MinimumLength 不能使用 fluent API 設(shè)定。 如前所述, MinimumLength 不會修改架構(gòu),而僅用于客戶端和服務(wù)端驗證規(guī)則。
有些開發(fā)人員中意排他的使用 fluent API 方式,這樣可以保持實體類干凈。 如果您愿意,可以混合使用特性和 fluent API 方式, 只有少數(shù)自定義項只能使用 fluent API 完成,但通常的做法是選擇其中的一種方式,并盡可能貫徹始終。 如果您同時使用兩者,請記住,當兩者設(shè)置產(chǎn)生沖突的時候, fluent API 將會覆蓋特性的設(shè)置。
有關(guān) attributes vs. fluent API 的更多信息,請參閱 Methods of configuration 。
顯示關(guān)系的實體圖表
下圖是使用 Entity Framework Power Tools 創(chuàng)建的完整學校模型的圖表。
除了一條“一對多”關(guān)系線外, 您還可以在 Instructor 和 OfficeAssignment 實體間看到一個“一對零或一”關(guān)系線(1 - 0..1),以及 Instructor 和 Department 實體間的“零或一對多”關(guān)系線(0..1 - *)。
給數(shù)據(jù)庫填充測試數(shù)據(jù)
使用以下代碼替換 Data/DbInitializer.cs 文件中的代碼,以提供新建實體的測試數(shù)據(jù)。
using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;
namespace ContosoUniversity.Data
{
public static class DbInitializer
{
public static void Initialize(SchoolContext context)
{
//context.Database.EnsureCreated();
// Look for any students.
if (context.Students.Any())
{
return; // DB has been seeded
}
var students = new 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") }
};
foreach (Student s in students)
{
context.Students.Add(s);
}
context.SaveChanges();
var instructors = new 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") }
};
foreach (Instructor i in instructors)
{
context.Instructors.Add(i);
}
context.SaveChanges();
var departments = new 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 }
};
foreach (Department d in departments)
{
context.Departments.Add(d);
}
context.SaveChanges();
var courses = new Course[]
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
},
};
foreach (Course c in courses)
{
context.Courses.Add(c);
}
context.SaveChanges();
var officeAssignments = new 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" },
};
foreach (OfficeAssignment o in officeAssignments)
{
context.OfficeAssignments.Add(o);
}
context.SaveChanges();
var courseInstructors = new CourseAssignment[]
{
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Harui").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
new CourseAssignment {
CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
},
};
foreach (CourseAssignment ci in courseInstructors)
{
context.CourseAssignments.Add(ci);
}
context.SaveChanges();
var enrollments = new 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();
}
}
}
正如您在第一篇教程中看到的, 大多數(shù)的代碼只是創(chuàng)建一個簡單的實體對象,并將數(shù)據(jù)寫入對應(yīng)的屬性中。請注意多對多關(guān)系是如何處理的:代碼通過創(chuàng)建 Enrollments 及 CourseAssignment 關(guān)聯(lián)實體集合來創(chuàng)建關(guān)系。
添加遷移
保存所做更改并生成項目。然后在項目文件夾打開命令窗口,輸入 migrations add 命令(暫時先別運行更新數(shù)據(jù)庫的命令):
dotnet ef migrations add ComplexDataModel
您會收到一條可能丟失數(shù)據(jù)的警告消息。
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'
如果您此時嘗試執(zhí)行數(shù)據(jù)庫更新命令的話,您會收到如下錯誤消息:
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database "ContosoUniversity", table "dbo.Department", column 'DepartmentID'.
有時候,當您在已有數(shù)據(jù)上執(zhí)行遷移,為了滿足外鍵約束,您需要在數(shù)據(jù)庫中插入一些 stub data (存根數(shù)據(jù),這個翻譯并不能讓人滿意,目前在有關(guān) TDD 測試驅(qū)動開發(fā)中也是用的這個翻譯)。 在生成的代碼中, Up 方法在 Course 表中添加了一個不可為空的 DepartmentID 外鍵。如果代碼運行時, Course 表中已經(jīng)存在數(shù)據(jù)行,則 AddColumn 操作將會失敗,因為 SQL Server 不知道在那一個不可為空的列中該放入何值。 對于本教程來說,您將在一個新的數(shù)據(jù)庫上運行遷移,但對于一個生產(chǎn)環(huán)境中的應(yīng)用程序來說,您必須想辦法對現(xiàn)有數(shù)據(jù)實現(xiàn)遷移,下面的說明展示一個如何執(zhí)行該操作的示例。
要在現(xiàn)有數(shù)據(jù)上讓遷移順利工作,您必須手工修改代碼以提供新建列一個默認值,創(chuàng)建一個名為 “temp” 的存根(stub)部門用于默認部門。結(jié)果是,在 Up 方法運行后,現(xiàn)有的課程將全部與 “Temp” 部門關(guān)聯(lián)。
- 打開 {時間戳}_ComplexDataModel.cs 文件。
- 注釋掉在 Course 表中添加 DepartmentID 列的代碼行。
migrationBuilder.AlterColumn<string>(
name: "Title",
table: "Course",
maxLength: 50,
nullable: true,
oldClrType: typeof(string),
oldNullable: true);
//migrationBuilder.AddColumn<int>(
// name: "DepartmentID",
// table: "Course",
// nullable: false,
// defaultValue: 0);
- 在創(chuàng)建 Department 表的代碼后面,加入下方高亮代碼:
migrationBuilder.CreateTable(
name: "Department",
columns: table => new
{
DepartmentID = table.Column<int>(nullable: false)
.Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
Budget = table.Column<decimal>(type: "money", nullable: false),
InstructorID = table.Column<int>(nullable: true),
Name = table.Column<string>(maxLength: 50, nullable: true),
StartDate = table.Column<DateTime>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Department", x => x.DepartmentID);
table.ForeignKey(
name: "FK_Department_Instructor_InstructorID",
column: x => x.InstructorID,
principalTable: "Instructor",
principalColumn: "ID",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.
migrationBuilder.AddColumn<int>(
name: "DepartmentID",
table: "Course",
nullable: false,
defaultValue: 1);
在生產(chǎn)環(huán)境的應(yīng)用程序中,您將會編寫代碼或腳本添加部門及部門關(guān)聯(lián)的課程。然后就不再需要 “Temp” 部門或 Course.DepartmentID 列的默認值。
保存所做的更改并生成項目。
更改連接字符串,并更新數(shù)據(jù)庫
現(xiàn)在您在 DbInitializer 類中有新代碼用于添加測試數(shù)據(jù)到一個空的數(shù)據(jù)庫。 要讓 EF 創(chuàng)建一個新的空數(shù)據(jù)庫, 可以通過修改 appsettings.json 中的連接字符串的數(shù)據(jù)庫名稱到 ContosoUniversity3 或者其他您的電腦未使用的名稱。
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
保存更改到 appsettings.json 。
備注
作為對不斷變化的數(shù)據(jù)庫名稱的替代方法,您可以刪除數(shù)據(jù)庫。 使用SQL Server 對象資源管理器(SSOX) 或database dropCLI 命令:
dotnet ef database drop
修改數(shù)據(jù)庫名稱或者刪除數(shù)據(jù)庫后, 在命令行窗口運行數(shù)據(jù)庫更新命令以讓遷移生效。
dotnet ef database update
運行應(yīng)用程序,DbInitializer.Initialize 方法運行并填充新的數(shù)據(jù)庫。
在 SSOX (SQL Server 資源管理器)打開數(shù)據(jù)庫, 展開表節(jié)點以查看是否已創(chuàng)建的所有表。(如果您的 SSOX 仍舊保留打開狀態(tài),則點擊刷新按鈕。)
運行應(yīng)用程序,觸發(fā)初始化代碼并填充測試數(shù)據(jù)到數(shù)據(jù)庫。
右鍵點擊 CourseAssignment 表,選擇“查看數(shù)據(jù)”來驗證其中確有數(shù)據(jù)。
小結(jié)
現(xiàn)在您有了一個更復(fù)雜的數(shù)據(jù)模型和對應(yīng)的數(shù)據(jù)庫。在以下教程中,你將了解有關(guān)如何訪問相關(guān)的數(shù)據(jù)的詳細信息。