Creating a complex data model 创建复杂数据模型
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.
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.
When you’re finished, the entity classes will make up the completed data model that’s shown in the following illustration:
Sections:
- Customize the Data Model by Using Attributes 使用Attributes定制数据模型
- Final changes to the Student entity Stduent实体的最终变化
- Create the Instructor Entity 创建讲师实体
- Create the OfficeAssignment entity 创建办公室分配实体
- Modify the Course Entity 修改课程实体
- Create the Department entity 创建学院实体
- Modify the Enrollment entity 修改注册实体
- Many-to-Many Relationships 多对多关系
- The CourseAssignment entity 课程分配实体
- Update the database context 更新数据库上下文
- Fluent API alternative to attributes Fluent API 与Attributes的关系
- Entity Diagram Showing Relationships 展示关系的实体框架表
- Seed the Database with Test Data 用测试数据初始化数据库
- Add a migration 增加迁移
- Change the connection string and update the database 变更连接字符串并且更新数据库
- Summary 总结
Customize the Data Model by Using Attributes 使用Attributes定制数据模型
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.
The DataType attribute
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.
对于学生注册日期,现在所有网页都是按照日期加时间的格式显示,但是你关注的仅仅是这个字段的日期。通过使用数据注解(data annotation)属性,可以实现通过修改一条代码而改变所有显示该项内容的视图。用一个例子演示如何去做,你将给Student类中的Enrollment属性添加一个属性。
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:
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 ICollectionEnrollments { 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属性是的得不到的:
- The browser can enable HTML5 features (for example to show a calendar control, the locale-appropriate currency symbol, email links, some client-side input validation, etc.).
- 浏览器可以使用HTML5特性(例如:显示一个日历控件、本地的货币符号、email链接、一些客户端输入验证,等等)。
- By default, the browser will render data using the correct format based on your locale.
- 默认情况下,浏览器将基于本地情况设置适当的数据格式。
For more information, see the tag helper documentation.
更多的信息,请参看 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.
The StringLength attribute
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 ICollectionEnrollments { 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 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.
The Column attribute
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 ICollectionEnrollments { 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:
保存变更并生成项目。然后,在项目文件夹中打开命令行窗口,并键入下列命令来创建另外一个迁移:
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.
如果你在完成下面章节中创建所有实体类的工作前进行编译,你将得到编译错误。
Final changes to the Student entity Student实体的最后变更
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 ICollectionEnrollments { get; set; } } }
The Table attribute
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
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”,从而替代了原特性名(单词中间没有空格)。
The FullName calculated property¶ FullName计算属性
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 the Instructor Entity¶ 新建讲师实体
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 ICollectionCourses { 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; } } }
The Key attribute¶ Key属性
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(不由数据库产生)来对待该主键, 因为该列用于验证关系。
The ForeignKey attribute¶ ForeignKey属性
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.
Modify the Course Entity¶ 修改Course实体
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 ICollectionEnrollments { get; set; } public ICollection 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¶ DatabaseGenerated属性
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 the Department entity¶
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 ICollectionCourses { get; set; } } }
The Column attribute¶
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类型。但是在这种情况下,你知道该列信息包含货币金额和货币类型是较为适合的。
Foreign key and navigation properties¶
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 ICollectionCourses { 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() .HasOne(d => d.Administrator) .WithMany() .OnDelete(DeleteBehavior.Restrict)
Modify the Enrollment entity¶ 修改注册实体
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; }
Many-to-Many Relationships¶ 多对多关系
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实体有这种多对多的关系,下一步将创建一个实体类,来定义一个没有有效载荷的连接表。
The CourseAssignment entity¶ 课程分配实体
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; } } }
Composite key¶ 复合键
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。
Join entity names¶ 连接实体名称
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。但是,推荐你选择表示关系的名称。建立数据模型从简单开始并逐渐扩展,在这个过程中,无效的联合逐渐变成有效的。如果你从描述明确的实体名称开始,你将不必在后续修改这个名称了。
Update the database context¶ 更新数据库上下文
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(DbContextOptionsoptions) : base(options) { } public DbSet Courses { get; set; } public DbSet Enrollments { get; set; } public DbSet Students { get; set; } public DbSet Departments { get; set; } public DbSet protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.EntityInstructors { get; set; } public DbSet OfficeAssignments { get; set; } public DbSet CourseAssignments { get; set; } ().ToTable("Course"); modelBuilder.Entity ().ToTable("Enrollment"); modelBuilder.Entity ().ToTable("Student"); modelBuilder.Entity ().ToTable("Department"); modelBuilder.Entity { c.CourseID, c.InstructorID }); } } }().ToTable("Instructor"); modelBuilder.Entity ().ToTable("OfficeAssignment"); modelBuilder.Entity ().ToTable("CourseAssignment"); modelBuilder.Entity () .HasKey(c => new
This code adds the new entities and configures the CourseAssignment entity’s composite primary key.
该代码添加并配置了CourseAssignment实体的复合主键。
Fluent API alternative to attributes¶
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() .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.
Entity Diagram Showing Relationships¶
The following illustration shows the diagram that the Entity Framework Power Tools create for the completed School model.
Seed the Database with Test Data¶
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.
Add a migration¶ 添加迁移
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
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( // 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.
Change the connection string and update the database¶
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.