ASP.NET Core 中的 Razor 页面和 EF Core - 数据模型 - 第 5 个教程(共 8 个)Razor Pages with EF Core in ASP.NET Core - Data Model - 5 of 8

作者:Tom DykstraRick AndersonBy Tom Dykstra and Rick Anderson

Contoso University Web 应用演示了如何使用 EF Core 和 Visual Studio 创建 Razor 页面 Web 应用。The Contoso University web app demonstrates how to create Razor Pages web apps using EF Core and Visual Studio. 若要了解系列教程,请参阅第一个教程For information about the tutorial series, see the first tutorial.

前面的教程介绍了由三个实体组成的基本数据模型。The previous tutorials worked with a basic data model that was composed of three entities. 本教程将演示如何:In this tutorial:

  • 添加更多实体和关系。More entities and relationships are added.
  • 通过指定格式设置、验证和数据库映射规则来自定义数据模型。The data model is customized by specifying formatting, validation, and database mapping rules.

已完成数据模型的实体类如下图所示:The entity classes for the completed data model is shown in the following illustration:

实体关系图

如果遇到无法解决的问题,请下载已完成应用If you run into problems you can't solve, download the completed app.

使用特性自定义数据模型Customize the data model with attributes

此部分将使用特性自定义数据模型。In this section, the data model is customized using attributes.

DataType 特性The DataType attribute

学生页面当前显示注册日期。The student pages currently displays the time of the enrollment date. 通常情况下,日期字段仅显示日期,不显示时间。Typically, date fields show only the date and not the time.

用以下突出显示的代码更新 Models/Student.csUpdate Models/Student.cs with the following highlighted code:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }

        public ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType 特性指定比数据库内部类型更具体的数据类型。The DataType attribute specifies a data type that's more specific than the database intrinsic type. 在此情况下,应仅显示日期,而不是日期加时间。In this case only the date should be displayed, not the date and time. DataType 枚举提供多种数据类型,例如日期、时间、电话号码、货币、电子邮件地址等。应用还可通过 DataType 特性自动提供类型特定的功能。The DataType Enumeration provides for many data types, such as Date, Time, PhoneNumber, Currency, EmailAddress, etc. The DataType attribute can also enable the app to automatically provide type-specific features. 例如:For example:

  • mailto: 链接将依据 DataType.EmailAddress 自动创建。The mailto: link is automatically created for DataType.EmailAddress.
  • 大多数浏览器中都提供面向 DataType.Date 的日期选择器。The date selector is provided for DataType.Date in most browsers.

DataType 特性发出 HTML 5 data-(读作 data dash)特性供 HTML 5 浏览器使用。The DataType attribute emits HTML 5 data- (pronounced data dash) attributes that HTML 5 browsers consume. DataType 特性不提供验证。The DataType attributes don't provide validation.

DataType.Date 不指定显示日期的格式。DataType.Date doesn't specify the format of the date that's displayed. 默认情况下,日期字段根据基于服务器的 CultureInfo 的默认格式进行显示。By default, the date field is displayed according to the default formats based on the server's CultureInfo.

DisplayFormat 特性用于显式指定日期格式:The DisplayFormat attribute is used to explicitly specify the date format:

[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

ApplyFormatInEditMode 设置指定还应对编辑 UI 应用该格式设置。The ApplyFormatInEditMode setting specifies that the formatting should also be applied to the edit UI. 某些字段不应使用 ApplyFormatInEditModeSome fields shouldn't use ApplyFormatInEditMode. 例如,编辑文本框中通常不应显示货币符号。For example, the currency symbol should generally not be displayed in an edit text box.

DisplayFormat 特性可由其本身使用。The DisplayFormat attribute can be used by itself. 搭配使用 DataType 特性和 DisplayFormat 特性通常是很好的做法。It's generally a good idea to use the DataType attribute with the DisplayFormat attribute. DataType 特性按照数据在屏幕上的呈现方式传达数据的语义。The DataType attribute conveys the semantics of the data as opposed to how to render it on a screen. DataType 特性可提供 DisplayFormat 中所不具有的以下优点:The DataType attribute provides the following benefits that are not available in DisplayFormat:

  • 浏览器可启用 HTML5 功能。The browser can enable HTML5 features. 例如,显示日历控件、区域设置适用的货币符号、电子邮件链接、客户端输入验证等。For example, show a calendar control, the locale-appropriate currency symbol, email links, client-side input validation, etc.
  • 默认情况下,浏览器将根据区域设置采用正确的格式呈现数据。By default, the browser renders data using the correct format based on the locale.

有关详细信息,请参阅 <input> 标记帮助器文档For more information, see the <input> Tag Helper documentation.

运行应用。Run the app. 导航到学生索引页。Navigate to the Students Index page. 将不再显示时间。Times are no longer displayed. 使用 Student 模型的每个视图将显示日期,不显示时间。Every view that uses the Student model displays the date without time.

“学生”索引页显示不带时间的日期

StringLength 特性The StringLength attribute

可使用特性指定数据验证规则和验证错误消息。Data validation rules and validation error messages can be specified with attributes. StringLength 特性指定数据字段中允许的字符的最小长度和最大长度。The StringLength attribute specifies the minimum and maximum length of characters that are allowed in a data field. StringLength 特性还提供客户端和服务器端验证。The StringLength attribute also provides client-side and server-side validation. 最小值对数据库架构没有任何影响。The minimum value has no impact on the database schema.

使用以下代码更新 Student 模型:Update the Student model with the following code:

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; }
    }
}

上面的代码将名称限制为不超过 50 个字符。The preceding code limits names to no more than 50 characters. StringLength 特性不会阻止用户在名称中输入空格。The StringLength attribute doesn't prevent a user from entering white space for a name. RegularExpression 特性用于向输入应用限制。The RegularExpression attribute is used 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:

[RegularExpression(@"^[A-Z]+[a-zA-Z""'\s-]*$")]

运行应用:Run the app:

  • 导航到学生页。Navigate to the Students page.
  • 选择“新建”并输入不超过 50 个字符的名称 。Select Create New, and enter a name longer than 50 characters.
  • 选择“创建”时,客户端验证会显示一条错误消息 。Select Create, client-side validation shows an error message.

显示字符串长度错误的“学生索引”页

在“SQL Server 对象资源管理器”(SSOX) 中,双击 Student 表,打开 Student 表设计器 。In SQL Server Object Explorer (SSOX), open the Student table designer by double-clicking the Student table.

迁移前 SSOX 中的 Student 表

上图显示 Student 表的架构。The preceding image shows the schema for the Student table. 名称字段的类型为 nvarchar(MAX),因为数据库上尚未运行迁移。The name fields have type nvarchar(MAX) because migrations has not been run on the DB. 稍后在本教程中运行迁移时,名称字段将变成 nvarchar(50)When migrations are run later in this tutorial, the name fields become nvarchar(50).

Column 特性The Column attribute

特性可以控制类和属性映射到数据库的方式。Attributes can control how classes and properties are mapped to the database. 在本部分,Column 特性用于将 FirstMidName 属性的名称映射到数据库中的“FirstName”。In this section, the Column attribute is used to map the name of the FirstMidName property to "FirstName" in the DB.

创建数据库后,模型上的属性名将用作列名(使用 Column 特性时除外)。When the DB is created, property names on the model are used for column names (except when the Column attribute is used).

Student 模型使用 FirstMidName 作为名字字段,因为该字段也可能包含中间名。The Student model uses FirstMidName for the first-name field because the field might also contain a middle name.

用以下突出显示的代码更新 Student.cs 文件:Update the Student.cs file with the following highlighted code:

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; }
    }
}

进行上述更改后,应用中的 Student.FirstMidName 将映射到 Student 表的 FirstName 列。With the preceding change, Student.FirstMidName in the app maps to the FirstName column of the Student table.

添加 Column 特性后,SchoolContext 的支持模型会发生改变。The addition of the Column attribute changes the model backing the SchoolContext. SchoolContext 的支持模型将不再与数据库匹配。The model backing the SchoolContext no longer matches the database. 如果在执行迁移前运行应用,则会生成如下异常:If the app is run before applying migrations, the following exception is generated:

SqlException: Invalid column name 'FirstName'.

若要更新数据库:To update the DB:

  • 生成项目。Build the project.
  • 在项目文件夹中打开命令窗口。Open a command window in the project folder. 输入以下命令以创建新迁移并更新数据库:Enter the following commands to create a new migration and update the DB:
Add-Migration ColumnFirstName
Update-Database

migrations add ColumnFirstName 命令将生成如下警告消息:The migrations add ColumnFirstName command generates the following warning message:

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.

生成警告的原因是名称字段现已限制为 50 个字符。The warning is generated because the name fields are now limited to 50 characters. 如果数据库中的名称超过 50 个字符,则第 51 个字符及后面的所有字符都将丢失。If a name in the DB had more than 50 characters, the 51 to last character would be lost.

  • 测试应用。Test the app.

在 SSOX 中打开 Student 表:Open the Student table in SSOX:

迁移后 SSOX 中的 Students 表

执行迁移前,名称列的类型为 nvarchar (MAX)Before migration was applied, the name columns were of type nvarchar(MAX). 名称列现在的类型为 nvarchar(50)The name columns are now nvarchar(50). 列名已从 FirstMidName 更改为 FirstNameThe column name has changed from FirstMidName to FirstName.

备注

在下一部分中,在某些阶段生成应用会生成编译器错误。In the following section, building the app at some stages generates compiler errors. 说明用于指定生成应用的时间。The instructions specify when to build the app.

Student 实体更新Student entity update

Student 实体

用以下代码更新 Models/Student.csUpdate Models/Student.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 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; }
    }
}

Required 特性The Required attribute

Required 特性使名称属性成为必填字段。The Required attribute makes the name properties required fields. 值类型(DateTimeintdouble)等不可为 NULL 的类型不需要 Required 特性。The Required attribute isn't needed for non-nullable types such as value types (DateTime, int, double, etc.). 系统会将不可为 NULL 的类型自动视为必填字段。Types that can't be null are automatically treated as required fields.

不能用 StringLength 特性中的最短长度参数替换 Required 特性:The Required attribute could be replaced with a minimum length parameter in the StringLength attribute:

[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }

Display 特性The Display attribute

Display 特性指定文本框的标题栏应为“FirstName”、“LastName”、“FullName”和“EnrollmentDate”。The Display attribute specifies that the caption for the text boxes should be "First Name", "Last Name", "Full Name", and "Enrollment Date." 标题栏默认不使用空格分隔词语,如“Lastname”。The default captions had no space dividing the words, for example "Lastname."

FullName 计算属性The FullName calculated property

FullName 是计算属性,可返回通过串联两个其他属性创建的值。FullName is a calculated property that returns a value that's created by concatenating two other properties. FullName 不能设置并且仅具有一个 get 访问器。FullName cannot be set, it has only a get accessor. 数据库中不会创建任何 FullName 列。No FullName column is created in the database.

创建 Instructor 实体Create the Instructor Entity

Instructor 实体

用以下代码创建 Models/Instructor.cs :Create Models/Instructor.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 Instructor
    {
        public int ID { get; set; }

        [Required]
        [Display(Name = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

        public ICollection<CourseAssignment> CourseAssignments { get; set; }
        public OfficeAssignment OfficeAssignment { get; set; }
    }
}

一行可包含多个特性。Multiple attributes can be on one line. 可按如下方式编写 HireDate 特性:The HireDate attributes could be written as follows:

[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

CourseAssignments 和 OfficeAssignment 导航属性The CourseAssignments and OfficeAssignment navigation properties

CourseAssignmentsOfficeAssignment 是导航属性。The CourseAssignments and OfficeAssignment properties are navigation properties.

一名讲师可以教授任意数量的课程,因此 CourseAssignments 定义为集合。An instructor can teach any number of courses, so CourseAssignments is defined as a collection.

public ICollection<CourseAssignment> CourseAssignments { get; set; }

如果导航属性包含多个实体:If a navigation property holds multiple entities:

  • 它必须是可在其中添加、删除和更新实体的列表类型。It must be a list type where the entries can be added, deleted, and updated.

导航属性类型包括:Navigation property types include:

  • ICollection<T>
  • List<T>
  • HashSet<T>

如果指定了 ICollection<T>,EF Core 会默认创建 HashSet<T> 集合。If ICollection<T> is specified, EF Core creates a HashSet<T> collection by default.

CourseAssignment 实体在“多对多关系”部分进行介绍。The CourseAssignment entity is explained in the section on many-to-many relationships.

Contoso University 业务规则规定一名讲师最多可获得一间办公室。Contoso University business rules state that an instructor can have at most one office. OfficeAssignment 属性包含一个 OfficeAssignment 实体。The OfficeAssignment property holds a single OfficeAssignment entity. 如果未分配办公室,则 OfficeAssignment 为 NULL。OfficeAssignment is null if no office is assigned.

public OfficeAssignment OfficeAssignment { get; set; }

创建 OfficeAssignment 实体Create the OfficeAssignment entity

OfficeAssignment 实体

用以下代码创建 Models/OfficeAssignment.cs :Create Models/OfficeAssignment.cs with the following code:

using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class OfficeAssignment
    {
        [Key]
        public int InstructorID { get; set; }
        [StringLength(50)]
        [Display(Name = "Office Location")]
        public string Location { get; set; }

        public Instructor Instructor { get; set; }
    }
}

Key 特性The Key attribute

[Key] 特性用于在属性名不是 classnameID 或 ID 时将属性标识为主键 (PK)。The [Key] attribute is used to identify a property as the primary key (PK) when the property name is something other than classnameID or ID.

InstructorOfficeAssignment 实体之间存在一对零或一关系。There's a one-to-zero-or-one relationship between the Instructor and OfficeAssignment entities. 仅当与分配到办公室的讲师之间建立关系时才存在办公室分配。An office assignment only exists in relation to the instructor it's assigned to. OfficeAssignment PK 也是其到 Instructor 实体的外键 (FK)。The OfficeAssignment PK is also its foreign key (FK) to the Instructor entity. EF Core 无法自动将 InstructorID 识别为 OfficeAssignment 的 PK,因为:EF Core can't automatically recognize InstructorID as the PK of OfficeAssignment because:

  • InstructorID 不遵循 ID 或 classnameID 命名约定。InstructorID doesn't follow the ID or classnameID naming convention.

因此,Key 特性用于将 InstructorID 识别为 PK:Therefore, the Key attribute is used to identify InstructorID as the PK:

[Key]
public int InstructorID { get; set; }

默认情况下,EF Core 将键视为非数据库生成,因为该列面向的是识别关系。By default, EF Core treats the key as non-database-generated because the column is for an identifying relationship.

Instructor 导航属性The Instructor navigation property

Instructor 实体的 OfficeAssignment 导航属性可以为 NULL,因为:The OfficeAssignment navigation property for the Instructor entity is nullable because:

  • 引用类型(例如,类可以为 NULL)。Reference types (such as classes are nullable).
  • 一名讲师可能没有办公室分配。An instructor might not have an office assignment.

OfficeAssignment 实体具有不可为 NULL 的 Instructor 导航属性,因为:The OfficeAssignment entity has a non-nullable Instructor navigation property because:

  • InstructorID 不可为 NULL。InstructorID is non-nullable.
  • 没有讲师则不可能存在办公室分配。An office assignment can't exist without an instructor.

Instructor 实体具有相关 OfficeAssignment 实体时,每个实体都具有对其导航属性中的另一个实体的引用。When an Instructor entity has a related OfficeAssignment entity, each entity has a reference to the other one in its navigation property.

[Required] 特性可以应用于 Instructor 导航属性:The [Required] attribute could be applied to the Instructor navigation property:

[Required]
public Instructor Instructor { get; set; }

上面的代码指定必须存在相关的讲师。The preceding code specifies that there must be a related instructor. 上面的代码没有必要,因为 InstructorID 外键(也是 PK)不可为 NULL。The preceding code is unnecessary because the InstructorID foreign key (which is also the PK) is non-nullable.

修改 Course 实体Modify the Course Entity

Course 实体

用以下代码更新 Models/Course.csUpdate Models/Course.cs 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> CourseAssignments { get; set; }
    }
}

Course 实体具有外键 (FK) 属性 DepartmentIDThe Course entity has a foreign key (FK) property DepartmentID. DepartmentID 指向相关的 Department 实体。DepartmentID points to the related Department entity. Course 实体具有 Department 导航属性。The Course entity has a Department navigation property.

当数据模型具有相关实体的导航属性时,EF Core 不要求此模型具有 FK 属性。EF Core doesn't require a FK property for a data model when the model has a navigation property for a related entity.

EF Core 可在数据库中的任何所需位置自动创建 FK。EF Core automatically creates FKs in the database wherever they're needed. EF Core 为自动创建的 FK 创建阴影属性EF Core creates shadow properties for automatically created FKs. 数据模型中包含 FK 后可使更新更简单和更高效。Having the FK in the data model can make updates simpler and more efficient. 例如,假设某个模型中不包含 FK 属性 DepartmentIDFor example, consider a model where the FK property DepartmentID is not included. 当提取 Course 实体进行编辑时:When a course entity is fetched to edit:

  • 如果未显式加载 Department 实体,则该实体将为 NULL。The Department entity is null if it's not explicitly loaded.
  • 若要更新 Course 实体,则必须先提取 Department 实体。To update the course entity, the Department entity must first be fetched.

如果数据模型中包含 FK 属性 DepartmentID,则无需在更新前提取 Department 实体。When the FK property DepartmentID is included in the data model, there's no need to fetch the Department entity before an update.

DatabaseGenerated 特性The DatabaseGenerated attribute

[DatabaseGenerated(DatabaseGeneratedOption.None)] 特性指定 PK 由应用程序提供而不是由数据库生成。The [DatabaseGenerated(DatabaseGeneratedOption.None)] attribute specifies that the PK is provided by the application rather than generated by the database.

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }

默认情况下,EF Core 假定 PK 值由数据库生成。By default, EF Core assumes that PK values are generated by the DB. 由数据库生成 PK 值通常是最佳方法。DB generated PK values is generally the best approach. Course 实体的 PK 由用户指定。For Course entities, the user specifies the PK. 例如,对于课程编号,数学系可以使用 1000 系列的编号,英语系可以使用 2000 系列的编号。For example, a course number such as a 1000 series for the math department, a 2000 series for the English department.

DatabaseGenerated 特性还可用于生成默认值。The DatabaseGenerated attribute can also be used to generate default values. 例如,数据库可以自动生成日期字段以记录数据行的创建或更新日期。For example, the DB can automatically generate a date field to record the date a row was created or updated. 有关详细信息,请参阅生成的属性For more information, see Generated Properties.

外键和导航属性Foreign key and navigation properties

Course 实体中的外键 (FK) 属性和导航属性可反映以下关系:The foreign key (FK) properties and navigation properties in the Course entity reflect the following relationships:

课程将分配到一个系,因此将存在 DepartmentID FK 和 Department 导航属性。A course is assigned to one department, so there's a DepartmentID FK and a Department navigation property.

public int DepartmentID { get; set; }
public Department Department { get; set; }

参与一门课程的学生数量不定,因此 Enrollments 导航属性是一个集合:A course can have any number of students enrolled in it, so the Enrollments navigation property is a collection:

public ICollection<Enrollment> Enrollments { get; set; }

一门课程可能由多位讲师讲授,因此 CourseAssignments 导航属性是一个集合:A course may be taught by multiple instructors, so the CourseAssignments navigation property is a collection:

public ICollection<CourseAssignment> CourseAssignments { get; set; }

CourseAssignment后文介绍。CourseAssignment is explained later.

创建 Department 实体Create the Department entity

Department 实体

用以下代码创建 Models/Department.cs :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; }
    }
}

Column 特性The Column attribute

Column 特性以前用于更改列名映射。Previously the Column attribute was used to change column name mapping. Department 实体的代码中,Column 特性用于更改 SQL 数据类型映射。In the code for the Department entity, the Column attribute is used to change SQL data type mapping. Budget 列通过数据库中的 SQL Server 货币类型进行定义:The Budget column is defined using the SQL Server money type in the DB:

[Column(TypeName="money")]
public decimal Budget { get; set; }

通常不需要列映射。Column mapping is generally not required. EF Core 通常基于属性的 CLR 类型选择相应的 SQL Server 数据类型。EF Core generally chooses the appropriate SQL Server data type based on the CLR type for the property. CLR decimal 类型会映射到 SQL Server decimal 类型。The CLR decimal type maps to a SQL Server decimal type. Budget 用于货币,但货币数据类型更适合货币。Budget is for currency, and the money data type is more appropriate for currency.

外键和导航属性Foreign key and navigation properties

FK 和导航属性可反映以下关系:The FK and navigation properties reflect the following relationships:

  • 一个系可能有也可能没有管理员。A department may or may not have an administrator.
  • 管理员始终由讲师担任。An administrator is always an instructor. 因此,InstructorID 属性作为到 Instructor 实体的 FK 包含在其中。Therefore the InstructorID property is included as the FK to the Instructor entity.

导航属性名为 Administrator,但其中包含 Instructor 实体:The navigation property is named Administrator but holds an Instructor entity:

public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }

上面代码中的问号 (?) 指定属性可以为 NULL。The question mark (?) in the preceding code specifies the property is nullable.

一个系可以有多门课程,因此存在 Course 导航属性:A department may have many courses, so there's a Courses navigation property:

public ICollection<Course> Courses { get; set; }

注意:按照约定,EF Core 能针对不可为 NULL 的 FK 和多对多关系启用级联删除。Note: By convention, EF Core enables cascade delete for non-nullable FKs and for many-to-many relationships. 级联删除可能导致形成循环级联删除规则。Cascading delete can result in circular cascade delete rules. 循环级联删除规则会在添加迁移时引发异常。Circular cascade delete rules causes an exception when a migration is added.

例如,如果 Department.InstructorID 属性定义为不可为 NULL:For example, if the Department.InstructorID property was defined as non-nullable:

  • EF Core 会配置级联删除规则,以在删除讲师时删除院系。EF Core configures a cascade delete rule to delete the department when the instructor is deleted.

  • 在删除讲师时删除院系并不是预期行为。Deleting the department when the instructor is deleted isn't the intended behavior.

  • 以下 fluent API 将设置限制规则而不是级联规则。The following fluent API would set a restrict rule instead of cascade.

    modelBuilder.Entity<Department>()
        .HasOne(d => d.Administrator)
        .WithMany()
        .OnDelete(DeleteBehavior.Restrict)
    

上面的代码会针对“系-讲师”关系禁用级联删除。The preceding code disables cascade delete on the department-instructor relationship.

更新 Enrollment 实体Update the Enrollment entity

一份注册记录面向一名学生所注册的一门课程。An enrollment record is for one course taken by one student.

Enrollment 实体

用以下代码更新 Models/Enrollment.csUpdate Models/Enrollment.cs 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

FK 属性和导航属性可反映以下关系:The FK properties and navigation properties reflect the following relationships:

注册记录面向一门课程,因此存在 CourseID FK 属性和 Course 导航属性:An enrollment record is for one course, so there's a CourseID FK property and a Course navigation property:

public int CourseID { get; set; }
public Course Course { get; set; }

一份注册记录针对一名学生,因此存在 StudentID FK 属性和 Student 导航属性:An enrollment record is for one student, so there's a StudentID FK property and a Student navigation property:

public int StudentID { get; set; }
public Student Student { get; set; }

多对多关系Many-to-Many Relationships

StudentCourse 实体之间存在多对多关系。There's a many-to-many relationship between the Student and Course entities. Enrollment 实体充当数据库中“具有有效负载”的多对多联接表 。The Enrollment entity functions as a many-to-many join table with payload in the database. “具有有效负载”表示 Enrollment 表除了联接表的 FK 外还包含其他数据(本教程中为 PK 和 Grade)。"With payload" means that the Enrollment table contains additional data besides FKs for the joined tables (in this case, the PK and Grade).

下图显示这些关系在实体关系图中的外观。The following illustration shows what these relationships look like in an entity diagram. (此关系图是使用适用于 EF 6.X 的 EF Power Tools 生成的。(This diagram was generated using EF Power Tools for EF 6.x. 本教程不介绍如何创建此关系图。)Creating the diagram isn't part of the tutorial.)

学生-课程之间的多对多关系

每条关系线的一端显示 1,另一端显示星号 (*),这表示一对多关系。Each relationship line has a 1 at one end and an asterisk (*) at the other, indicating a one-to-many relationship.

如果 Enrollment 表不包含年级信息,则它只需包含两个 FK(CourseIDStudentID)。If the Enrollment table didn't include grade information, it would only need to contain the two FKs (CourseID and StudentID). 无有效负载的多对多联接表有时称为纯联接表 (PJT)。A many-to-many join table without payload is sometimes called a pure join table (PJT).

InstructorCourse 实体具有使用纯联接表的多对多关系。The Instructor and Course entities have a many-to-many relationship using a pure join table.

注意:EF 6.x 支持多对多关系的隐式联接表,但 EF Core 不支持。Note: EF 6.x supports implicit join tables for many-to-many relationships, but EF Core doesn't. 有关详细信息,请参阅 EF Core 2.0 中的多对多关系For more information, see Many-to-many relationships in EF Core 2.0.

CourseAssignment 实体The CourseAssignment entity

CourseAssignment 实体

用以下代码创建 Models/CourseAssignment.cs :Create Models/CourseAssignment.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 CourseAssignment
    {
        public int InstructorID { get; set; }
        public int CourseID { get; set; }
        public Instructor Instructor { get; set; }
        public Course Course { get; set; }
    }
}

讲师-课程Instructor-to-Courses

讲师-课程 m:M

讲师-课程的多对多关系:The Instructor-to-Courses many-to-many relationship:

  • 要求必须用实体集表示联接表。Requires a join table that must be represented by an entity set.
  • 为纯联接表(无有效负载的表)。Is a pure join table (table without payload).

常规做法是将联接实体命名为 EntityName1EntityName2It's common to name a join entity EntityName1EntityName2. 例如,使用此模式的“讲师-课程”联接表是 CourseInstructorFor example, the Instructor-to-Courses join table using this pattern is CourseInstructor. 但是,我们建议使用可描述关系的名称。However, we recommend using a name that describes the relationship.

数据模型开始时很简单,其内容会逐渐增加。Data models start out simple and grow. 无有效负载联接 (PJT) 通常会发展为包含有效负载。No-payload joins (PJTs) frequently evolve to include payload. 该名称以描述性实体名称开始,因此不需要随联接表更改而更改。By starting with a descriptive entity name, the name doesn't need to change when the join table changes. 理想情况下,联接实体在业务领域中可能具有专业名称(可能是一个词)。Ideally, the join entity would have its own natural (possibly single word) name in the business domain. 例如,可以使用名为“阅读率”的联接实体链接“书籍”和“读客”。For example, Books and Customers could be linked with a join entity called Ratings. 对于“讲师-课程”的多对多关系,使用 CourseAssignment 比使用CourseInstructor更合适。For the Instructor-to-Courses many-to-many relationship, CourseAssignment is preferred over CourseInstructor.

组合键Composite key

FK 不能为 NULL。FKs are not nullable. CourseAssignment 中的两个 FK(InstructorIDCourseID)共同唯一标识 CourseAssignment 表的每一行。The two FKs in CourseAssignment (InstructorID and CourseID) together uniquely identify each row of the CourseAssignment table. CourseAssignment 不需要专用的 PK。CourseAssignment doesn't require a dedicated PK. InstructorIDCourseID 属性充当组合 PK。The InstructorID and CourseID properties function as a composite PK. 使用 Fluent API 是向 EF Core 指定组合 PK 的唯一方法 。The only way to specify composite PKs to EF Core is with the fluent API. 下一部分演示如何配置组合 PK。The next section shows how to configure the composite PK.

组合键可确保:The composite key ensures:

  • 允许一门课程对应多行。Multiple rows are allowed for one course.
  • 允许一名讲师对应多行。Multiple rows are allowed for one instructor.
  • 不允许相同的讲师和课程对应多行。Multiple rows for the same instructor and course isn't allowed.

Enrollment 联接实体定义其自己的 PK,因此可能会出现此类重复。The Enrollment join entity defines its own PK, so duplicates of this sort are possible. 若要防止此类重复:To prevent such duplicates:

  • 请在 FK 字段上添加唯一索引,或Add a unique index on the FK fields, or
  • 配置具有主要组合键(与 CourseAssignment 类似)的 EnrollmentConfigure Enrollment with a primary composite key similar to CourseAssignment. 有关详细信息,请参阅索引For more information, see Indexes.

更新数据库上下文Update the DB context

将以下突出显示的代码添加到 Data/SchoolContext.cs :Add the following highlighted code to Data/SchoolContext.cs:

using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;

namespace ContosoUniversity.Models
{
    public class SchoolContext : DbContext
    {
        public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
        {
        }

        public DbSet<Course> Courses { get; set; }
        public DbSet<Enrollment> Enrollment { get; set; }
        public DbSet<Student> Student { 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 });
        }
    }
}

上面的代码添加新实体并配置 CourseAssignment 实体的组合 PK。The preceding code adds the new entities and configures the CourseAssignment entity's composite PK.

用 Fluent API 替代特性Fluent API alternative to attributes

上面代码中的 OnModelCreating 方法使用 Fluent API 配置 EF Core 行为 。The OnModelCreating method in the preceding code uses the fluent API to configure EF Core behavior. API 称为“Fluent”,因为它通常在将一系列方法调用连接成单个语句后才能使用。The API is called "fluent" because it's often used by stringing a series of method calls together into a single statement. 下面的代码是 Fluent API 的示例:The following code is an example of the fluent API:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Property(b => b.Url)
        .IsRequired();
}

在本教程中,Fluent API 仅用于不能通过特性完成的数据库映射。In this tutorial, the fluent API is used only for DB mapping that can't be done with attributes. 但是,Fluent API 可以指定可通过特性完成的大多数格式设置、验证和映射规则。However, the fluent API can specify most of the formatting, validation, and mapping rules that can be done with attributes.

MinimumLength 等特性不能通过 Fluent API 应用。Some attributes such as MinimumLength can't be applied with the fluent API. MinimumLength 不会更改架构,它仅应用最小长度验证规则。MinimumLength doesn't change the schema, it only applies a minimum length validation rule.

某些开发者倾向于仅使用 Fluent API 以保持实体类的“纯净”。Some developers prefer to use the fluent API exclusively so that they can keep their entity classes "clean." 特性和 Fluent API 可以相互混合。Attributes and the fluent API can be mixed. 某些配置只能通过 Fluent API 完成(指定组合 PK)。There are some configurations that can only be done with the fluent API (specifying a composite PK). 有些配置只能通过特性完成 (MinimumLength)。There are some configurations that can only be done with attributes (MinimumLength). 使用 Fluent API 或特性的建议做法:The recommended practice for using fluent API or attributes:

  • 选择以下两种方法之一。Choose one of these two approaches.
  • 尽可能以前后一致的方法使用所选的方法。Use the chosen approach consistently as much as possible.

本教程中使用的某些特性可用于:Some of the attributes used in the this tutorial are used for:

  • 仅限验证(例如,MinimumLength)。Validation only (for example, MinimumLength).
  • 仅限 EF Core 配置(例如,HasKey)。EF Core configuration only (for example, HasKey).
  • 验证和 EF Core 配置(例如,[StringLength(50)])。Validation and EF Core configuration (for example, [StringLength(50)]).

有关特性和 Fluent API 的详细信息,请参阅配置方法For more information about attributes vs. fluent API, see Methods of configuration.

显示关系的实体关系图Entity Diagram Showing Relationships

下图显示 EF Power Tools 针对已完成的学校模型创建的关系图。The following illustration shows the diagram that EF Power Tools create for the completed School model.

实体关系图

上面的关系图显示:The preceding diagram shows:

  • 几条一对多关系线(1 到 *)。Several one-to-many relationship lines (1 to *).
  • InstructorOfficeAssignment 实体之间的一对零或一关系线(1 到 0..1)。The one-to-zero-or-one relationship line (1 to 0..1) between the Instructor and OfficeAssignment entities.
  • InstructorDepartment 实体之间的零或一到多关系线(0..1 到 *)。The zero-or-one-to-many relationship line (0..1 to *) between the Instructor and Department entities.

使用测试数据为数据库设定种子Seed the DB with Test Data

更新 Data/DbInitializer.cs 中的代码 :Update the code in Data/DbInitializer.cs:

using System;
using System.Linq;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using ContosoUniversity.Models;

namespace ContosoUniversity.Data
{
    public static class DbInitializer
    {
        public static void Initialize(SchoolContext context)
        {
            //context.Database.EnsureCreated();

            // Look for any students.
            if (context.Student.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.Student.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.Enrollment.Where(
                    s =>
                            s.Student.ID == e.StudentID &&
                            s.Course.CourseID == e.CourseID).SingleOrDefault();
                if (enrollmentInDataBase == null)
                {
                    context.Enrollment.Add(e);
                }
            }
            context.SaveChanges();
        }
    }
}

前面的代码为新实体提供种子数据。The preceding code provides seed data for the new entities. 大多数此类代码会创建新实体对象并加载示例数据。Most of this code creates new entity objects and loads sample data. 示例数据用于测试。The sample data is used for testing. 有关如何对多对多联接表进行种子设定的示例,请参阅 EnrollmentsCourseAssignmentsSee Enrollments and CourseAssignments for examples of how many-to-many join tables can be seeded.

添加迁移Add a migration

生成项目。Build the project.

Add-Migration ComplexDataModel

前面的命令显示可能存在数据丢失的相关警告。The preceding command displays a warning about possible data loss.

An operation was scaffolded that may result in the loss of data.
Please review the migration for accuracy.
Done. To undo this action, use 'ef migrations remove'

如果运行 database update 命令,则会生成以下错误:If the database update command is run, the following error is produced:

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'.

应用迁移Apply the migration

现已有一个数据库,需要考虑如何将未来的更改应用到其中。Now that you have an existing database, you need to think about how to apply future changes to it. 本教程演示两种方法:This tutorial shows two approaches:

  • 删除并重新创建数据库Drop and re-create the database
  • 将迁移应用到现有数据库Apply the migration to the existing database. 虽然此方法更复杂且耗时,但在实际应用和生产环境中为首选方法。While this method is more complex and time-consuming, it's the preferred approach for real-world, production environments. 说明:这是本教程的一个可选部分。Note: This is an optional section of the tutorial. 你可以执行删除和重新创建的相关步骤并跳过此部分。You can do the drop and re-create steps and skip this section. 如果希望执行本部分中的步骤,请勿执行删除和重新创建步骤。If you do want to follow the steps in this section, don't do the drop and re-create steps.

删除并重新创建数据库Drop and re-create the database

已更新 DbInitializer 中的代码将为新实体添加种子数据。The code in the updated DbInitializer adds seed data for the new entities. 若要强制 EF Core 创建新的 DB,请删除并更新 DB:To force EF Core to create a new DB, drop and update the DB:

在“包管理器控制台”(PMC) 中运行以下命令 :In the Package Manager Console (PMC), run the following command:

Drop-Database
Update-Database

从 PMC 运行 Get-Help about_EntityFrameworkCore,获取帮助信息。Run Get-Help about_EntityFrameworkCore from the PMC to get help information.

运行应用。Run the app. 运行应用后将运行 DbInitializer.Initialize 方法。Running the app runs the DbInitializer.Initialize method. DbInitializer.Initialize 将填充新数据库。The DbInitializer.Initialize populates the new DB.

在 SSOX 中打开数据库:Open the DB in SSOX:

  • 如果之前已打开过 SSOX,请单击“刷新”按钮 。If SSOX was opened previously, click the Refresh button.
  • 展开“表”节点 。Expand the Tables node. 随后将显示出已创建的表。The created tables are displayed.

SSOX 中的表

查看 CourseAssignment 表 :Examine the CourseAssignment table:

  • 右键单击 CourseAssignment 表,然后选择“查看数据” 。Right-click the CourseAssignment table and select View Data.
  • 验证 CourseAssignment 表包含数据 。Verify the CourseAssignment table contains data.

SSOX 中的 CourseAssignment 数据

将迁移应用到现有数据库Apply the migration to the existing database

本部分是可选的。This section is optional. 只有当跳过之前的删除并重新创建数据库部分时才可以执行上述步骤。These steps work only if you skipped the preceding Drop and re-create the database section.

当现有数据与迁移一起运行时,可能存在不满足现有数据的 FK 约束。When migrations are run with existing data, there may be FK constraints that are not satisfied with the existing data. 使用生产数据时,必须采取步骤来迁移现有数据。With production data, steps must be taken to migrate the existing data. 本部分提供修复 FK 约束冲突的示例。This section provides an example of fixing FK constraint violations. 务必在备份后执行这些代码更改。Don't make these code changes without a backup. 如果已完成上述部分并更新数据库,则不要执行这些代码更改。Don't make these code changes if you completed the previous section and updated the database.

{timestamp}_ComplexDataModel.cs 文件包含以下代码 :The {timestamp}_ComplexDataModel.cs file contains the following code:

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    type: "int",
    nullable: false,
    defaultValue: 0);

上面的代码将向 Course 表添加不可为 NULL 的 DepartmentID FK。The preceding code adds a non-nullable DepartmentID FK to the Course table. 前面教程中的数据库在 Course 中包含行,以便迁移时不会更新表。The DB from the previous tutorial contains rows in Course, so that table cannot be updated by migrations.

若要使 ComplexDataModel 迁移可与现有数据搭配运行:To make the ComplexDataModel migration work with existing data:

  • 请更改代码以便为新列 (DepartmentID) 赋予默认值。Change the code to give the new column (DepartmentID) a default value.
  • 创建名为“临时”的虚拟系来充当默认的系。Create a fake department named "Temp" to act as the default department.

修复外键约束Fix the foreign key constraints

更新 ComplexDataModelUp 方法:Update the ComplexDataModel classes Up method:

  • 打开 {timestamp}_ComplexDataModel.cs 文件 。Open the {timestamp}_ComplexDataModel.cs file.
  • 对将 DepartmentID 列添加到 Course 表的代码行添加注释。Comment out the line of code that adds the DepartmentID column to the Course table.
migrationBuilder.AlterColumn<string>(
    name: "Title",
    table: "Course",
    maxLength: 50,
    nullable: true,
    oldClrType: typeof(string),
    oldNullable: true);
            
//migrationBuilder.AddColumn<int>(
//    name: "DepartmentID",
//    table: "Course",
//    nullable: false,
//    defaultValue: 0);

添加以下突出显示的代码。Add the following highlighted code. 新代码在 .CreateTable( name: "Department" 块后:The new code goes after the .CreateTable( name: "Department" block:

migrationBuilder.CreateTable(
    name: "Department",
    columns: table => new
    {
        DepartmentID = table.Column<int>(type: "int", nullable: false)
            .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn),
        Budget = table.Column<decimal>(type: "money", nullable: false),
        InstructorID = table.Column<int>(type: "int", nullable: true),
        Name = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
        StartDate = table.Column<DateTime>(type: "datetime2", nullable: false)
    },
    constraints: table =>
    {
        table.PrimaryKey("PK_Department", x => x.DepartmentID);
        table.ForeignKey(
            name: "FK_Department_Instructor_InstructorID",
            column: x => x.InstructorID,
            principalTable: "Instructor",
            principalColumn: "ID",
            onDelete: ReferentialAction.Restrict);
    });

 migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// Default value for FK points to department created above, with
// defaultValue changed to 1 in following AddColumn statement.

migrationBuilder.AddColumn<int>(
    name: "DepartmentID",
    table: "Course",
    nullable: false,
    defaultValue: 1);

经过上面的更改,Course 行将在 ComplexDataModel Up 方法运行后与“临时”系建立联系。With the preceding changes, existing Course rows will be related to the "Temp" department after the ComplexDataModel Up method runs.

生产应用可能:A production app would:

  • 包含用于将 Department 行和相关 Course 行添加到新 Department 行的代码或脚本。Include code or scripts to add Department rows and related Course rows to the new Department rows.
  • 不会使用“临时”系或 Course.DepartmentID 的默认值。Not use the "Temp" department or the default value for Course.DepartmentID.

下一教程将介绍相关数据。The next tutorial covers related data.

其他资源Additional resources