The Contoso University sample web application demonstrates how to create ASP.NET Core 1.0 MVC web applications using Entity Framework Core 1.0 and Visual Studio 2015. For information about the tutorial series, see the first tutorial in the series.html
In the previous tutorials you worked with a simple data model that was composed of three entities. In this tutorial you’ll add more entities and relationships and you’ll customize the data model by specifying formatting, validation, and database mapping rules.node
When you’re finished, the entity classes will make up the completed data model that’s shown in the following illustration:ios
Sections:web
In this section you’ll see how to customize the data model by using attributes that specify formatting, validation, and database mapping rules. Then in several of the following sections you’ll create the complete School data model by adding attributes to the classes you already created and creating new classes for the remaining entity types in the model.sql
DateType屬性數據庫
For student enrollment dates, all of the web pages currently display the time along with the date, although all you care about for this field is the date. By using data annotation attributes, you can make one code change that will fix the display format in every view that shows the data. To see an example of how to do that, you’ll add an attribute to the EnrollmentDate
property in the Student
class.json
對於學生註冊日期,如今全部網頁都是按照日期加時間的格式顯示,可是你關注的僅僅是這個字段的日期。經過使用數據註解(data annotation)屬性,能夠實現經過修改一條代碼而改變全部顯示該項內容的視圖。用一個例子演示如何去作,你將給Student類中的Enrollment屬性添加一個屬性。c#
In Models/Student.cs, add a using
statement for the System.ComponentModel.DataAnnotations
namespace and add DataType
and DisplayFormat
attributes to the EnrollmentDate
property, as shown in the following example:api
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; } } }
The DataType
attribute is used to specify a data type that is more specific than the database intrinsic type. In this case we only want to keep track of the date, not the date and time. The DataType
Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, and more. The DataType
attribute can also enable the application to automatically provide type-specific features. For example, a mailto:
link can be created for DataType.EmailAddress
, and a date selector can be provided for DataType.Date
in browsers that support HTML5. The DataType
attribute emits HTML 5 data-
(pronounced data dash) attributes that HTML 5 browsers can understand. The DataType
attributes do not provide any validation.瀏覽器
DataType屬性用於指定比數據庫內部類型更具體化的數據類型。在本例中,咱們只想保留日期,而不是日期加時間。DateType枚舉提供許多數據類型,例如:日期、時間、電話號碼、貨幣、電郵地址等等。DataType屬性也能使應用自動提供類型指定功能。例如,DataType.EmailAddress可生成mailto:連接,DataType.Date可向支持HTML5的瀏覽器一個日期選擇器。DataType屬性傳遞給支持HTML5的瀏覽器一個data-(以data-打頭的聲明)。DataType屬性不提供任何驗證功能。
DataType.Date
does not specify the format of the date that is displayed. By default, the data field is displayed according to the default formats based on the server’s CultureInfo.
DateType.Date並不指定日期的顯示格式。默認狀況下,顯示的數據根據服務器文化信息(國別)的默認格式進行顯示。
The DisplayFormat
attribute is used to explicitly specify the date format:
DisplayFormat屬性用於明確指定日期的格式:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
The ApplyFormatInEditMode
setting specifies that the formatting should also be applied when the value is displayed in a text box for editing. (You might not want that for some fields – for example, for currency values, you might not want the currency symbol in the text box for editing.)
ApplyFormatInEditMode設置指定了在文件框的編輯狀態下仍然採用該顯示格式。(你可能不想這樣作,例如:對於貨幣值來講,你可能不想在文本框中進行編輯時仍然顯示貨幣符號)。
You can use the DisplayFormat
attribute by itself, but it’s generally a good idea to use the DataType
attribute also. The DataType
attribute conveys the semantics of the data as opposed to how to render it on a screen, and provides the following benefits that you don’t get with DisplayFormat
:
你可僅使用DisplayFormat屬性,可是同時使用DateType屬性一般是個更好的方法。DataType屬性傳遞了數據的語義,(若是僅使用DisplayFormat屬性)則僅僅是說明了如何呈現在屏幕上,而且提供了下列優勢,這些優勢從DisplayFormat屬性是的得不到的:
For more information, see the <input> tag helper documentation.
更多的信息,請參看<input> tag helper documentation.
Run the Students Index page again and notice that times are no longer displayed for the enrollment dates. The same will be true for any view that uses the Student model.
StringLength屬性
You can also specify data validation rules and validation error messages using attributes. The StringLength
attribute sets the maximum length in the database and provides client side and server side validation for ASP.NET MVC. You can also specify the minimum string length in this attribute, but the minimum value has no impact on the database schema.
也可使用屬性來指定數據驗證規則和相關的驗證錯誤信息。StringLength屬性設置了數據庫中該字段的最大長度,而且向ASP.NET MVC提供客戶端和服務器端驗證。你也可在該屬性中指定最小字符串長度,可是最小長度值對於數據庫結構來講是沒有必要的。
Suppose you want to ensure that users don’t enter more than 50 characters for a name. To add this limitation, add StringLength
attributes to the LastName
and FirstMidName
properties, as shown in the following example:
using System; using System.Collections.Generic; 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 = "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; } } }
The StringLength
attribute won’t prevent a user from entering white space for a name. You can use the RegularExpression
attribute to apply restrictions to the input. For example the following code requires the first character to be upper case and the remaining characters to be alphabetical:
StringLength屬性不會阻止用戶在名字中輸入空格。你可使用RegulrExpression屬性來限制輸入。例如,下列代碼表示:第一個字符是大寫,將其他的字符是英文字母。
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'\s]*$")]
The MaxLength
attribute provides functionality similar to the StringLength
attribute but doesn’t provide client side validation.
MaxLength屬性提供的功能與StringLength屬性相似,但不能提供客戶端的驗證。
The database model has now changed in a way that requires a change in the database schema. You’ll use migrations to update the schema without losing any data that you may have added to the database by using the application UI.
如今數據模型變化了,進而須要更改數據庫結構。你將使用遷移功能更新數據庫結構,在這個過程當中並不會丟失那些可能經過應用界面已經增長到數據庫內的數據。
Save your changes and build the project. Then open the command window in the project folder and enter the following commands:
dotnet ef migrations add MaxLengthOnNames -c SchoolContext
dotnet ef database update -c SchoolContext
The migrations add
command creates a file named <timeStamp>_MaxLengthOnNames.cs. This file contains code in the Up
method that will update the database to match the current data model. The database update
command ran that code.
migrations add 命令建立了名爲<時間戳>_MaxLengthOnNames.cs的文件。該文件中的Up方法包含的代碼將更新數據庫以匹配數據模型。database update命令將執行這些代碼。
The timestamp prefixed to the migrations file name is used by Entity Framework to order the migrations. You can create multiple migrations before running the update-database command, and then all of the migrations are applied in the order in which they were created.
EF利用遷移文件名稱重前置的時間戳將遷移進行排序。你可在運行update-database命令前建立多個遷移,接下來全部的遷移會按照建立順序執行。
Run the Create page, and enter either name longer than 50 characters. When you click Create, client side validation shows an error message.
You can also use attributes to control how your classes and properties are mapped to the database. Suppose you had used the name FirstMidName
for the first-name field because the field might also contain a middle name. But you want the database column to be named FirstName
, because users who will be writing ad-hoc queries against the database are accustomed to that name. To make this mapping, you can use the Column
attribute.
The Column
attribute specifies that when the database is created, the column of the Student
table that maps to the FirstMidName
property will be named FirstName
. In other words, when your code refers to Student.FirstMidName
, the data will come from or be updated in the FirstName
column of the Student
table. If you don’t specify column names, they are given the same name as the property name.
Column屬性說明了:當建立數據庫的時候,映射到FirstMidName屬性的Stduent表中的列將被命名爲FirstName。換句話說,當你的代碼指向Stduents.FirstMidName時,數據未來自或者更新到Student表的FirstName列。若是你不指定列名,列名將與屬性名相同。
In the Student.cs file, add a using
statement for System.ComponentModel.DataAnnotations.Schema
and add the column name attribute to the FirstMidName
property, as shown in the following highlighted code:
在Student.cs文件中,增長一條using語句----System.ComponentModel.DataAnnotations.Schema,而且給FirstMidName屬性增長一個列名屬性,就像下面代碼中的高亮部分:
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; } } }
Save your changes and build the project.
保存變動並生成項目。
The addition of the Column
attribute changes the model backing the SchoolContext
, so it won’t match the database.
增長的Column屬性改變了SchoolContext中的模型,因此這將與數據庫內的信息再也不相同。
Save your changes and build the project. Then open the command window in the project folder and enter the following commands to create another migration:
保存變動並生成項目。而後,在項目文件夾中打開命令行窗口,並鍵入下列命令來建立另一個遷移:
dotnet ef migrations add ColummFirstName -c SchoolContext
dotnet ef database update -c SchoolContext
In SQL Server Object Explorer, open the Student table designer by double-clicking the Student table.
Before you applied the first two migrations, the name columns were of type nvarchar(MAX). They are now nvarchar(50) and the column name has changed from FirstMidName to FirstName.
Note
If you try to compile before you finish creating all of the entity classes in the following sections, you might get compiler errors.
若是你在完成下面章節中建立全部實體類的工做前進行編譯,你將獲得編譯錯誤。
In Models/Student.cs, replace the code you added earlier with the following code. The changes are highlighted.
在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; } [Required] [StringLength(50)] [Display(Name = "Last Name")] public string LastName { get; set; } [Required] [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")] [Column("FirstName")] [Display(Name = "First Name")] 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; } [Display(Name = "Full Name")] public string FullName { get { return LastName + ", " + FirstMidName; } } public ICollection<Enrollment> Enrollments { get; set; } } }
Table屬性
As you saw in the first tutorial, by default tables are named after the DbSet
property name. The property name is for a collection, so it is typically plural (「Students」), but many developers and DBAs prefer to use the singular form (「Student」) for table names. This attribute specifies the name that EF will use for the table in the database that stores Student entities.
正如在第一篇教程中看到的,默認按照DbSet屬性名後面的名字給數據庫的表進行命名。然而,屬性名錶明瞭一個集合,因此通常地採用複數形式(「Students」),可是許多開發者和數據庫系統更喜歡用單數形式命名錶名。這個屬性指定了EF將在數據庫中存儲Stduent實體的表名。
The Required
attribute makes the name properties required fields. The Required
attribute is not needed for non-nullable types such as value types (DateTime, int, double, float, etc.). Types that can’t be null are automatically treated as required fields.
Required屬性將字段屬性規定成是必須填寫的。對於非空類型來講Required屬性不是必須的(例如數值類型:DateTime, int, double, float等等)。非空類型被自動看成必須填寫的字段。
You could remove the Required
attribute and replace it with a minimum length parameter for the StringLength
attribute:
你能夠刪除Required屬性,而且將其替換爲限制最小長度的參數------StringLength屬性:
[Display(Name = "Last Name")] [StringLength(50, MinimumLength=1)] public string LastName { get; set; }
The Display attribute¶
The Display
attribute specifies that the caption for the text boxes should be 「First Name」, 「Last Name」, 「Full Name」, and 「Enrollment Date」 instead of the property name in each instance (which has no space dividing the words).
Display屬性指定了文本框的標題爲「First Name」、「Last Name"、"Full Name"以及「Enrollment Date」,從而替代了原特性名(單詞中間沒有空格)。
FullName
is a calculated property that returns a value that’s created by concatenating two other properties. Therefore it has only a get accessor, and no FullName
column will be generated in the database.
FullName是一個被計算出來的屬性,返回由其餘兩個屬性鏈接後造成的值。所以,它只有get運算符,而且在數據庫中不生成FullName列。
Create Models/Instructor.cs, replacing the template code with the following code:
新建Models/Instrctor.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> Courses { get; set; } public OfficeAssignment OfficeAssignment { get; set; } } }
Notice that several properties are the same in the Student and Instructor entities. In the Implementing Inheritance tutorial later in this series, you’ll refactor this code to eliminate the redundancy.
請注意,有幾個屬性與Stduent和Instructor實體相同。在本系列教程後面的Implementing Inheritance (實施繼承)章節,你將重構這些代碼以消除這些冗餘。
You can put multiple attributes on one line, so you could also write the HireDate
attributes as follows:
你能夠將多個屬性放在同一行中,因此也可寫爲:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
The Courses and OfficeAssignment navigation properties
Create Models/OfficeAssignment.cs with the following code:
使用下列代碼新建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; } } }
There’s a one-to-zero-or-one relationship between the Instructor and the OfficeAssignment entities. An office assignment only exists in relation to the instructor it’s assigned to, and therefore its primary key is also its foreign key to the Instructor entity. But the Entity Framework can’t automatically recognize InstructorID as the primary key of this entity because its name doesn’t follow the ID or classnameID naming convention. Therefore, the Key
attribute is used to identify it as the key:
在Instructor和OfficeAssignment實體間存在1對0或對1的關係。一個辦公室的安排僅與安排到辦公室的講師存在關係,所以它的主鍵以及外鍵是Instructor實體。但實體框架由於它的名字並不遵循的 ID 或 classnameID 的命名約定,不能自動識別 InstructorID 爲此實體的主鍵。所以,Key屬性用於將其識別的主鍵。
[Key] [ForeignKey("Instructor")] public int InstructorID { get; set; }
You can also use the Key
attribute if the entity does have its own primary key but you want to name the property something other than classnameID or ID.
若是實體確實具備主鍵,但你想命名成其餘的名稱,而不是classnameID或者ID,你也可以使用Key屬性。
By default EF treats the key as non-database-generated because the column is for an identifying relationship.
默認狀況下,EF按non-database-generated(不禁數據庫產生)來對待該主鍵, 由於該列用於驗證關係。
When there is a one-to-zero-or-one relationship or a one-to-one relationship between two entities (such as between OfficeAssignment and Instructor), EF might not be able to work out which end of the relationship is the principal and which end is dependent. One-to-one relationships have a reference navigation property in each class to the other class. The ForeignKey
attribute can be applied to the dependent class to establish the relationship.
In Models/Course.cs, replace the code you added earlier with the following code:
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> Assignments { get; set; } } }
The course entity has a foreign key property DepartmentID
which points to the related Department entity and it has a Department
navigation property.
Course實體有一個DepartmentID外鍵屬性,指向相關的Department實體,同時也有一個Department導航屬性。
The Entity Framework doesn’t require you to add a foreign key property to your data model when you have a navigation property for a related entity. EF automatically creates foreign keys in the database wherever they are needed and creates shadow properties for them. But having the foreign key in the data model can make updates simpler and more efficient. For example, when you fetch a course entity to edit, the Department entity is null if you don’t load it, so when you update the course entity, you would have to first fetch the Department entity. When the foreign key property DepartmentID
is included in the data model, you don’t need to fetch the Department entity before you update.
當有一個和某實體相關導航屬性時,EF不須要你給數據模型添加外鍵屬性。EF會在數據庫中自動建立外鍵,不管是否須要都會爲其建立shadow properties。可是,在數據模型中擁有外鍵可以使更新更加簡單和更富效率。例如,當你獲得一個course實體要編輯時,若是不加載Department,則Department實體是空的,因此當你更新course實體時,你將必須首先得到Deparment實體。當外鍵屬性DepartmentID包含在數據模型中時,你不須要在更新前得到Department實體。
The DatabaseGenerated
attribute with the None
parameter on the CourseID
property specifies that primary key values are provided by the user rather than generated by the database.
CourseID屬性上面的DatabaseGenerated屬性帶有None參數,它指定了由用戶提供主鍵值,而不是由數據庫產生。
[DatabaseGenerated(DatabaseGeneratedOption.None)] [Display(Name = "Number")] public int CourseID { get; set; }
By default, the Entity Framework assumes that primary key values are generated by the database. That’s what you want in most scenarios. However, for Course entities, you’ll use a user-specified course number such as a 1000 series for one department, a 2000 series for another department, and so on.
默認狀況下,EF假定由數據庫生成主鍵。這在大多數場景中是這樣的。然而,對於Course實體,你會使用由用戶指定的課程編碼,例如1000系列由一個系使用,2000系列由另一個系使用等等。
The DatabaseGenerated
attribute can also be used to generate default values, as in the case of database columns used to record the date a row was created or updated. For more information, see Generated Properties.
DatabaseGenerated屬性還可用於生成默認值,好比當數據庫的列用於記錄一個行被建立或更新的日期。更多的信息,請參看Generated Properties。
Create Models/Department.cs with the following code:
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; } } }
Earlier you used the Column
attribute to change column name mapping. In the code for the Department entity, the Column
attribute is being used to change SQL data type mapping so that the column will be defined using the SQL Server money type in the database:
早前使用Column屬性改變了映射的列名。在Department實體代碼中,Column屬性被用於改變映射的SQL數據類型,因此在數據庫中該列將被定義成SQL Server的money類型。
[Column(TypeName="money")] public decimal Budget { get; set; }
Column mapping is generally not required, because the Entity Framework chooses the appropriate SQL Server data type based on the CLR type that you define for the property. The CLR decimal
type maps to a SQL Server decimal
type. But in this case you know that the column will be holding currency amounts, and the money data type is more appropriate for that.
一般狀況下列映射是不須要的,由於EF基於你設定屬性的CLR類型來選擇適當的SQL Server數據類型。CLR的decimal類型映射到SQL Server的decimal類型。可是在這種狀況下,你知道該列信息包含貨幣金額和貨幣類型是較爲適合的。
The foreign key and navigation properties reflect the following relationships:
A department may or may not have an administrator, and an administrator is always an instructor. Therefore the InstructorID
property is included as the foreign key to the Instructor entity, and a question mark is added after the int
type designation to mark the property as nullable. The navigation property is named Administrator
but holds an Instructor entity:
public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
A department may have many courses, so there’s a Courses navigation property:
public ICollection<Course> Courses { get; set; }
Note
By convention, the Entity Framework enables cascade delete for non-nullable foreign keys and for many-to-many relationships. This can result in circular cascade delete rules, which will cause an exception when you try to add a migration. For example, if you didn’t define the Department.InstructorID property as nullable, EF would configure a cascade delete rule to delete the instructor when you delete the department, which is not what you want to have happen. If your business rules required the InstructorID
property to be non-nullable, you would have to use the following fluent API statement to disable cascade delete on the relationship:
按照約定,EF對非空外鍵和多對多關係啓用級聯刪除。這可致使循環級聯刪除規則,當你嘗試增長遷移時這將致使異常。例如,若是你沒有將Department.InstrctiorID定義爲非空類型,當你刪除department時,EF將配置一個級聯刪除規則來刪除講師,這將不是你所想要的。若是你的商務規則須要InstructorID屬性做爲非空的,你將必須使用下列fluent API語句來關閉關係的級聯刪除。
modelBuilder.Entity<Department>() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
In Models/Enrollment.cs, replace the code you added earlier with the following code:
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; } } }
Foreign key and navigation properties¶ 外鍵和導航屬性
The foreign key properties and navigation properties reflect the following relationships:
外鍵屬性和導航屬性反應瞭如下關係:
An enrollment record is for a single course, so there’s a CourseID
foreign key property and a Course
navigation property:
一條註冊記錄與單個課程對應,因此有一個CourseID外鍵屬性和一個Course導航屬性:
public int CourseID { get; set; } public Course Course { get; set; }
An enrollment record is for a single student, so there’s a StudentID
foreign key property and a Student
navigation property:
一條註冊記錄與一個學生對應,因此有一個StduentID外鍵屬性和一個Student導航屬性:
public int StudentID { get; set; } public Student Student { get; set; }
There’s a many-to-many relationship between the Student and Course entities, and the Enrollment entity functions as a many-to-many join table with payload in the database. 「With payload」 means that the Enrollment table contains additional data besides foreign keys for the joined tables (in this case, a primary key and a Grade property).
在Student和Course實體間有多對多的關係,Enrollment實體做爲多對多的橋樑,將數據庫中的表和有效載荷進行鏈接。"鏈接成有效載荷"意味着Enrollment表除了鏈接表的外鍵以外,還包含了一些附加的數據(在本例中,包含了主鍵和Grade屬性)。
The following illustration shows what these relationships look like in an entity diagram. (This diagram was generated using the Entity Framework Power Tools for EF 6.x; creating the diagram isn’t part of the tutorial, it’s just being used here as an illustration.)
下面的圖示形象地展示了實體圖中的關係。(該圖由Entity Framework Power Tools for EF 6.x。如何生成該圖不屬於本教程的範圍,僅用來圖示說明)
Each relationship line has a 1 at one end and an asterisk (*) at the other, indicating a one-to-many relationship.
每條表明關係的線一頭是1,另外一頭是星號(*),說明該關係是1對多的關係。
If the Enrollment table didn’t include grade information, it would only need to contain the two foreign keys CourseID and StudentID. In that case, it would be a many-to-many join table without payload (or a pure join table) in the database. The Instructor and Course entities have that kind of many-to-many relationship, and your next step is to create an entity class to function as a join table without payload.
若是Enrollment表不包含Grade的信息,則僅包含CourseID和StudentID兩個外鍵。在那種狀況下,這將成爲一個多對多的鏈接表,並不包含有效載荷(或者說是一個純鏈接表)。Instuctor和Course實體有這種多對多的關係,下一步將建立一個實體類,來定義一個沒有有效載荷的鏈接表。
A join table is required in the database for the Instructor-to-Courses many-to-many relationship, and CourseAssignment
is the entity that represents that table.
數據庫中的鏈接表用於表達例如「講師-課程」之間的多對多關係,CourseAssignment就是表達這種表的實體。
Create Models/CourseAssignment.cs with the following code:
用下列代碼新建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; } } }
Since the foreign keys are not nullable and together uniquely identify each row of the table, there is no need for a separate primary key. The InstructorID and CourseID properties should function as a composite primary key. The only way to identify composite primary keys to EF is by using the fluent API (it can’t be done by using attributes). You’ll see how to configure the composite primary key in the next section.
由於外鍵是非空,而且每一行都驗證是惟一的,因此不須要單獨的主鍵。InstructorID和CourseID屬性將承擔複合鍵的做用。EF定義複合鍵的惟一方法是使用fluent API(使用屬性是作不到的)。下一章,你將看到如何配置複合主鍵。
The composite key ensures that while you can have multiple rows for one course, and multiple rows for one instructor, you can’t have multiple rows for the same instructor and course. The Enrollment
join entity defines its own primary key, so duplicates of this sort are possible. To prevent such duplicates, you could add a unique index on the foreign key fields, or configure Enrollment
with a primary composite key similar to CourseAssignment
. For more information, see Indexes.
複合鍵保證了當一門課程有多行對應,而且一名講師有多行對應的狀況下,對於同一個講師和課程不會發生有多個行對應的狀況。Enrollment聯合實體定義了本身的主鍵,因此實現上述功能成爲可能。要避免這樣作,可在外鍵字段上增長一個惟一索引,或者給Enrollment配置一個相似於CoureAssignment的複合主鍵。更多的信息,請參看Indexs。
It’s common to name a join entity EntityName1EntityName2
, which in this case would be CourseInstructor
. However, we recommend that you choose a name that describes the relationship. Data models start out simple and grow, with no-payload joins frequently getting payloads later. If you start with a descriptive entity name, you won’t have to change the name later.
一般狀況下,將聯合實體命名爲EntityName1EntityName2,在這裏是CourseInstructor。可是,推薦你選擇表示關係的名稱。創建數據模型從簡單開始並逐漸擴展,在這個過程當中,無效的聯合逐漸變成有效的。若是你從描述明確的實體名稱開始,你將沒必要在後續修改這個名稱了。
Add the following highlighted code to the Data/SchoolContext.cs:
在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"); 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 }); } } }
This code adds the new entities and configures the CourseAssignment entity’s composite primary key.
該代碼添加並配置了CourseAssignment實體的複合主鍵。
The code in the OnModelCreating
method of the DbContext
class uses the fluent API to configure EF behavior. The API is called 「fluent」 because it’s often used by stringing a series of method calls together into a single statement, as in this example from the EF Core documentation:
DbContext類中的OnModelCreating方法使用了fluent API來配置EF的行爲。該API之因此被命名爲「fluent」是由於,它常常在一條語句中被連成一個方法串。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Blog>() .Property(b => b.Url) .IsRequired(); }
In this tutorial you’re using the fluent API only for database mapping that you can’t do with attributes. However, you can also use the fluent API to specify most of the formatting, validation, and mapping rules that you can do by using attributes. Some attributes such as MinimumLength
can’t be applied with the fluent API. As mentioned previously, MinimumLength
doesn’t change the schema, it only applies a client and server side validation rule.
在本教程中,你將僅使用fluent API處理不能用屬性進行設置的數據庫映射關係。然而,你也可以使用fluent API來指定格式、驗證以及映射關係等大部分可使用屬性來完成的工做。例如MinimumLength等一些屬性不能經過fluent API來設定。如同前面談到的,MinimumLength不能改變schema,其僅是進行客戶端和服務器端的驗證功能。
Some developers prefer to use the fluent API exclusively so that they can keep their entity classes 「clean.」 You can mix attributes and fluent API if you want, and there are a few customizations that can only be done by using fluent API, but in general the recommended practice is to choose one of these two approaches and use that consistently as much as possible. If you do use both, note that wherever there is a conflict, Fluent API overrides attributes.
一些開發者更願意使用fluent API,以便保持實體看起來更清晰。若是願意,你能夠混合使用屬性和fluent API,有一些定製化的工做只能經過使用fluent API來實現,但一般推薦的方法是儘量的擇其一而使用。若是二者都使用的話,要注意之間是否存在矛盾,若是存在矛盾,Fluent API要在屬性之上。
For more information about attributes vs. fluent API, see Methods of configuration.
The following illustration shows the diagram that the Entity Framework Power Tools create for the completed School model.
Replace the code in the Data/DbInitializer.cs file with the following code in order to provide seed data for the new entities you’ve created.
using System; using System.Linq; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using ContosoUniversity.Data; namespace ContosoUniversity.Models { 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(); } } }
As you saw in the first tutorial, most of this code simply creates new entity objects and loads sample data into properties as required for testing. Notice how the many-to-many relationships are handled: the code creates relationships by creating entities in the Enrollments
and CourseInstructor
join entity sets.
Save your changes and build the project. Then open the command window in the project folder and enter the migrations add
command (don’t do the update-database command yet):
保存變動,而且build項目。而後在項目文件夾中打開命令行窗口,鍵入migrations add命令(還不要使用update-database命令):
dotnet ef migrations add ComplexDataModel -c SchoolContext
You get a warning about possible data loss.
你獲得可能丟失數據的警告。
C:\ContosoUniversity\src\ContosoUniversity>dotnet ef migrations add ComplexDataModel -c SchoolContext Project ContosoUniversity (.NETCoreApp,Version=v1.0) will be compiled because Input items removed from last build Compiling ContosoUniversity for .NETCoreApp,Version=v1.0 Compilation succeeded. 0 Warning(s) 0 Error(s) Time elapsed 00:00:02.9907258 An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy. Done. To undo this action, use 'dotnet ef migrations remove'
If you tried to run the database update
command at this point (don’t do it yet), you would get the following error:
若是這時你嘗試運行database update命令(請先別這樣作),你將獲得下述錯誤:
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’.
Sometimes when you execute migrations with existing data, you need to insert stub data into the database to satisfy foreign key constraints. The generated code in the Up
method adds a non-nullable DepartmentID foreign key to the Course table. If there are already rows in the Course table when the code runs, the AddColumn
operation fails because SQL Server doesn’t know what value to put in the column that can’t be null. For this tutorial you’ll run the migration on a new database, but in a production application you’d have to make the migration handle existing data, so the following directions show an example of how to do that.
有些時候,當你使用已有數據進行遷移的時候,須要將存根數據插入數據庫來知足外鍵約束。在Up方法生成的代碼中,給Course表增長一個非空的DepartmentID外鍵。若是運行代碼時,在Course表中已經有這些行,AddColumn操做就會失敗,由於SQL Server不知道向這些非空的列中放置什麼值。在該教程中,你將在一個新數據庫中執行遷移,可是在生產應用中,你沒必要用遷移來處理已經存在的數據,因此下面的指導展現了一個例子,來講明如何這樣作。
To make this migration work with existing data you have to change the code to give the new column a default value, and create a stub department named 「Temp」 to act as the default department. As a result, existing Course rows will all be related to the 「Temp」 department after the Up
method runs.
要在已經存在的數據上作遷移工做,你必須改變代碼,將一個默認值賦給新列,建立一個名爲「Temp」的存根department做爲department的默認值。在運行Up方法後,已有Course的行將所有與"Temp"department關聯起來。
Open the <timestamp>_ComplexDataModel.cs file. Comment out the line of code that adds the DepartmentID column to the Course table, and add before it the following code (the commented lines are also shown):
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); //migrationBuilder.AddColumn<int>( // name: "DepartmentID", // table: "Course", // nullable: false, // defaultValue: 0);
In a production application, you would write code or scripts to add Department rows and relate Course rows to the new Department rows. You would then no longer need the 「Temp」 department or the default value on the Course.DepartmentID column.
Save your changes and build the project.
You now have new code in the DbInitializer
class that adds seed data for the new entities to an empty database. To make EF create a new empty database, change the name of the database in the connection string in appsettings.json to ContosoUniversity3 or some other name that you haven’t used on the computer you’re using.
{ "ConnectionStrings": { "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true" },
Save your change to appsettings.json.
Note
As an alternative to changing the database name, you can delete the database. Use SQL Server Object Explorer (SSOX) or the database drop
CLI command:
dotnet ef database drop -c SchoolContext
After you have changed the database name or deleted the database, run the database update
command in the command window to execute the migrations.
dotnet ef database update -c SchoolContext
Run the app to cause the DbInitializer.Initialize
method to run and populate the new database.
Open the database in SSOX as you did earlier, and expand the Tables node to see that all of the tables have been created. (If you still have SSOX open from the earlier time, click the Refresh button.)
Run the application to trigger the initializer code that seeds the database.
Right-click the CourseInstructors table and select View Data to verify that it has data in it.
You now have a more complex data model and corresponding database. In the following tutorial, you’ll learn more about how to access related data.