在前面的教程中,你使用了由三个实体组成的简单数据模型。 在本教程中,你将添加更多实体和关系,并通过指定格式、验证和数据库映射规则来自定义数据模型。
完成本教程后,实体类将构成下图所示的完整数据模型:
在本教程中,你将了解:
- 自定义数据模型
- 对 Student 实体进行更改
- 创建讲师实体
- 创建 OfficeAssignment 实体
- 修改 Course 实体
- 创建部门实体
- 修改登记实体
- 更新数据库上下文
- 使用测试数据填充数据库
- 添加迁移
- 更改连接字符串
- 更新数据库
Prerequisites
自定义数据模型
本节将展示如何通过使用指定格式、验证和数据库映射规则的属性来自定义数据模型。 然后,在以下几个部分中,你将通过向已创建的类添加属性并为模型中的剩余实体类型创建新类来创建完整的学校数据模型。
DataType 属性
对于学生注册日期,目前所有网页都显示有时间和日期,尽管对此字段而言重要的只是日期。 使用数据注释特性,可更改一次代码,修复每个视图中数据的显示格式。 若要查看如何执行此操作,请向 EnrollmentDate 类的 Student 属性添加一个特性。
在 中,为 命名空间添加 语句,并为 属性添加 和 属性,如以下示例所示:
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 属性用于指定比数据库内部类型更具体的数据类型。 在此示例中,我们只想跟踪日期,而不是日期和时间。
DataType枚举提供许多数据类型,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。 应用程序还可通过 DataType 特性自动提供类型特定的功能。 例如,可以为 mailto: 创建 DataType.EmailAddress 链接,并且可以在支持 HTML5 的浏览器中为 DataType.Date 提供日期选择器。 该 DataType 属性生成 HTML 5 data-(读作 data dash)属性,HTML 5 浏览器可以理解这些属性。 这些 DataType 属性不提供任何有效性检查。
DataType.Date 不指定显示日期的格式。 默认情况下,数据字段根据服务器 CultureInfo 的默认格式显示。
DisplayFormat 特性用于显式指定日期格式:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode 设置指定在值显示于文本框以进行编辑时,也应应用此格式。 (你可能不希望某些字段这样做——例如,对于货币值,编辑时,你可能不希望文本框中显示货币符号。)
可以单独使用该 DisplayFormat 属性,但通常最好也使用该 DataType 属性。 该 DataType 属性传达数据的语义,而不是如何在屏幕上呈现数据,并提供以下使用 DisplayFormat 无法获得的好处:
浏览器可以启用 HTML5 功能(例如,显示日历控件、适合区域设置的货币符号、电子邮件链接、某些客户端输入验证等)。
默认情况下,浏览器将根据区域设置采用正确的格式呈现数据。
有关详细信息,请参阅 <输入> 标记帮助程序文档。
运行应用,转到“学生索引”页,并注意到注册日期不再显示时间。 对于使用 Student 模型的任何视图,同样如此。
StringLength 属性
还可以使用属性指定数据验证规则和验证错误消息。 该StringLength属性设置数据库中的最大长度,并为 ASP.NET Core MVC 提供客户端验证和服务器端验证。 还可在此属性中指定最小字符串长度,但最小值对数据库架构没有影响。
假设要确保用户输入的名称不超过 50 个字符。 若要添加此限制,请将StringLength特性添加到LastName和FirstMidName属性,如以下示例所示:
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)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
该 StringLength 属性不会阻止用户在名称中输入空白字符。 可以使用该 RegularExpression 属性将限制应用于输入。 例如,以下代码要求第一个字符为大写,其余字符必须为字母:
[RegularExpression(@"^[A-Z]+[a-zA-Z]*$")]
该 MaxLength 属性提供的功能类似于 StringLength 属性,但不提供客户端验证。
数据库模型现已发生变化,因此需要更改数据库架构。 你将使用迁移来更新架构,而不会丢失可能已使用应用程序 UI 添加到数据库的任何数据。
保存更改并生成项目。 然后打开项目文件夹中的命令窗口并输入以下命令:
dotnet ef migrations add MaxLengthOnNames
dotnet ef database update
该 migrations add 命令警告可能会发生数据丢失,因为更改使两列的最大长度更短。 迁移将创建一个名为 <timeStamp>_MaxLengthOnNames.cs.. 此文件包含 Up 方法中的代码,该方法将更新数据库以匹配当前数据模型。 该 database update 命令执行了该代码。
Entity Framework 使用前缀到迁移文件名的时间戳对迁移进行排序。 可以在运行 update-database 命令之前创建多个迁移,然后按照创建迁移的顺序应用所有迁移。
运行应用,选择“学生”选项卡,单击“新建”,然后尝试输入超过 50 个字符的任一名称。 应用程序应阻止你执行此操作。
Column 属性
还可使用特性来控制类和属性映射到数据库的方式。 假设在名字字段使用了 FirstMidName,这是因为该字段也可能包含中间名。 希望将数据库列命名为FirstName,因为对数据库进行即席查询的用户习惯使用这个名称。 若要进行此映射,可使用 Column 特性。
Column 特性指定,创建数据库时,映射到 Student 属性的 FirstMidName 表的列将被命名为 FirstName。 换言之,在代码引用 Student.FirstMidName 时,数据来源将是 FirstName 表的 Student 列或在其中进行更新。 如果未指定列名,则会为它们提供与属性名称相同的名称。
在 Student.cs 文件中,为 System.ComponentModel.DataAnnotations.Schema 添加一个 using 语句,并将列名称属性添加到 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)]
[Column("FirstName")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public ICollection<Enrollment> Enrollments { get; set; }
}
}
添加 Column 属性会更改 SchoolContext 背后的模型,因此它将与数据库不匹配。
保存更改并生成项目。 然后打开项目文件夹中的命令窗口,并输入以下命令以创建另一个迁移:
dotnet ef migrations add ColumnFirstName
dotnet ef database update
在SQL Server 对象资源管理器中,双击Student表以打开 Student 表设计器。
在应用前两个迁移之前,名称列的类型为 nvarchar(MAX)。 它们现在为 nvarchar(50),列名称已从 FirstMidName 更改为 FirstName。
Note
如果在完成以下部分中的所有实体类创建之前尝试编译,则可能会收到编译器错误。
对 Student 实体的更改
在 Models/Student.cs 中,将前面添加的代码替换为以下代码。 这些更改已被突出显示。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50)]
[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 属性
Required 特性使名称属性成为必填字段。 不可为 null 的类型(例如值类型(DateTime、int、double、float 等)不需要此属性 Required 。 系统会将不可为 NULL 的类型自动视为必填字段。
Required 特性必须与 MinimumLength 结合使用,才能使 MinimumLength 生效。
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
Display 属性
该 Display 属性指定,文本框的标题应为“名字”“姓氏”“全名”和“注册日期”,而不是每个实例中的属性名称(其单词之间没有空格)。
FullName 计算属性
FullName 是计算属性,可返回通过串联两个其他属性创建的值。 因此,它只有 get 访问器,不会在数据库中生成 FullName 列。
创建讲师实体
创建 Models/Instructor.cs,将模板代码替换为以下代码:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Instructor
{
public int ID { get; set; }
[Required]
[Display(Name = "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; }
}
}
请注意,学生和讲师实体中的多个属性相同。 在本系列后面的“实现继承”教程中,你将重构此代码以消除冗余。
可以将多个属性放在一行上,因此也可以按如下所示编写 HireDate 属性:
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
CourseAssignments 和 OfficeAssignment 导航属性
CourseAssignments 和 OfficeAssignment 是导航属性。
一名讲师可以教授任意数量的课程,因此将 CourseAssignments 定义为一个集合。
public ICollection<CourseAssignment> CourseAssignments { get; set; }
如果导航属性可以保存多个实体,则其类型必须是一个列表,可在其中添加、删除和更新条目。 你可以指定 ICollection<T>,或者指定诸如 List<T> 或 HashSet<T> 之类的类型。 如果指定 ICollection<T>,EF在默认情况下创建 HashSet<T> 集合。
这些之所以是 CourseAssignment 实体的原因将在下文关于多对多关系的部分中说明。
Contoso University 商业规则规定,讲师最多只能有一个办公室,因此该 OfficeAssignment 属性包含一个 OfficeAssignment 实体(如果未分配任何办公室,该实体可能为 null)。
public OfficeAssignment OfficeAssignment { get; set; }
创建 OfficeAssignment 实体
使用以下代码创建 Models/OfficeAssignment.cs:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public Instructor Instructor { get; set; }
}
}
Key 属性
Instructor 和 OfficeAssignment 实体之间存在一对零或一关系。 工作岗位分配仅与分配给其的讲师相关联,所以其主键也是 Instructor 实体的外键。 但 Entity Framework 无法自动识别 InstructorID 为此实体的主键,因为它的名称不遵循 ID 或 classnameID 命名约定。 因此,Key 特性用于将其识别为主键:
[Key]
public int InstructorID { get; set; }
如果实体确实有自己的主键,但想要将属性命名为 classnameID 或 ID 以外的名称,也可以使用 Key 属性。
默认情况下,EF 将该键视为非数据库生成的,因为该列用于标识性关系。
Instructor 导航属性
Instructor 实体具有可以为 null 的 OfficeAssignment 导航属性(因为讲师可能没有办公室分配),并且 OfficeAssignment 实体具有不可为 null 的 Instructor 导航属性(因为没有讲师的办公室分配不存在 -- InstructorID 不可为 null)。 当讲师实体具有相关的 OfficeAssignment 实体时,每个实体都将在其导航属性中具有对另一个实体的引用。
可以在 Instructor 导航属性上添加 [Required] 特性,以指定必须有关联的讲师,但不必这样做,因为 InstructorID 外键(也是此表的键)不可为 null。
修改 Course 实体
在中 Models/Course.cs,将前面添加的代码替换为以下代码。 这些更改已被突出显示。
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "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; }
}
}
课程实体具有外键属性 Department,该属性指向相关的 Department 实体,并且它还具有一个 DepartmentID 导航属性。
如果拥有相关实体的导航属性,则 Entity Framework 不会要求为数据模型添加外键属性。 EF 会根据需要在数据库中自动创建外键,并为它们创建 阴影属性 。 但如果数据模型包含外键,则更新会变得更简单、更高效。 例如,提取要编辑的 Course 实体时,如果未加载实体,则该 Department 实体为 null,因此在更新 Course 实体时,必须首先提取 Department 实体。 数据模型中包含外键属性 DepartmentID 时,更新前无需提取 Department 实体。
DatabaseGenerated 特性
DatabaseGenerated 特性上带有 CourseID 参数的 None 属性指定主键值由用户提供,而不是由数据库生成。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
默认情况下,Entity Framework 假定主键值由数据库生成。 大多数情况下,这是理想情况。 但对 Course 实体而言,需使用用户指定的课程编号,例如一个系为 1000 系列,另一个系为 2000 系列等。
该 DatabaseGenerated 属性还可用于生成默认值,就像用于记录行创建或更新日期的数据库列一样。 有关详细信息,请参阅生成的属性。
外键和导航属性
Course 实体中的外键属性和导航属性可反映以下关系:
向一个系分配课程后,出于上述原因,会出现 DepartmentID 外键和 Department 导航属性。
public int DepartmentID { get; set; }
public Department Department { get; set; }
参与一门课程的学生数量不定,因此 Enrollments 导航属性是一个集合:
public ICollection<Enrollment> Enrollments { get; set; }
课程可能由多个讲师教授,因此CourseAssignments导航属性是一个集合(该类型将在CourseAssignment解释):
public ICollection<CourseAssignment> CourseAssignments { get; set; }
创建部门实体
使用以下代码创建 Models/Department.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Department
{
public int DepartmentID { get; set; }
[StringLength(50, MinimumLength = 3)]
public string Name { get; set; }
[DataType(DataType.Currency)]
[Column(TypeName = "money")]
public decimal Budget { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Start Date")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
public ICollection<Course> Courses { get; set; }
}
}
Column 属性
之前,你使用了该 Column 属性来更改列名称映射。 在 Department 实体的代码中,该 Column 属性用于更改 SQL 数据类型映射,以便在数据库中使用 SQL Server money 类型来定义该列:
[Column(TypeName="money")]
public decimal Budget { get; set; }
通常不需要列映射,因为实体框架根据为属性定义的 CLR 类型选择适当的SQL Server数据类型。 CLR decimal 类型会映射到 SQL Server decimal 类型。 但在这种情况下,你知道该列将持有货币金额,并且货币数据类型更适合该列。
外键和导航属性
外键和导航属性可反映以下关系:
一个系可能有也可能没有管理员,而管理员始终是讲师。 因此,包含 InstructorID 属性作为 Instructor 实体的外键,并在 int 类型标注后添加问号,将该属性标记为可为 null。 导航属性已命名为 Administrator,但包含一个 Instructor 实体:
public int? InstructorID { get; set; }
public Instructor Administrator { get; set; }
一个系可以有多门课程,因此存在 Course 导航属性:
public ICollection<Course> Courses { get; set; }
Note
按照约定,Entity Framework 能针对不可为 null 的外键和多对多关系启用级联删除。 这可能会导致循环级联删除规则,这会导致在尝试添加迁移时出现异常。 例如,如果未将 Department.InstructorID 属性定义为可为 null,EF 会配置级联删除规则,以便在删除讲师时删除部门,而这不是你想要执行的操作。 如果业务规则要求 InstructorID 属性不可为 null,则必须使用以下 Fluent API 语句来禁用关系上的级联删除:
modelBuilder.Entity<Department>()
.HasOne(d => d.Administrator)
.WithMany()
.OnDelete(DeleteBehavior.Restrict)
修改登记实体
在Models/Enrollment.cs中,将前面添加的代码替换为以下代码:
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public enum Grade
{
A, B, C, D, F
}
public class Enrollment
{
public int EnrollmentID { get; set; }
public int CourseID { get; set; }
public int StudentID { get; set; }
[DisplayFormat(NullDisplayText = "No grade")]
public Grade? Grade { get; set; }
public Course Course { get; set; }
public Student Student { get; set; }
}
}
外键和导航属性
外键属性和导航属性可反映以下关系:
注册记录针对一门课程,因此有一个CourseID 外键属性和一个Course 导航属性:
public int CourseID { get; set; }
public Course Course { get; set; }
注册记录面向一名学生,因此存在 StudentID 外键属性和 Student 导航属性:
public int StudentID { get; set; }
public Student Student { get; set; }
多对多关系
Student 和 Course 实体间存在多对多关系,Enrollment 实体在数据库中充当带有效负载的多对多联接表。 “使用有效负载”表示 Enrollment 表包含联接表外键以外的其他数据(在本例中,主键和 Grade 属性)。
下图显示这些关系在实体关系图中的外观。 (此关系图是使用 Entity Framework Power Tools for EF 6.x 生成的;创建关系图不是本教程的一部分,它只是在此处用作插图。)
每条关系线的一端显示 1,另一端显示星号 (*),这表示一对多关系。
如果 Enrollment 表不包含年级信息,则它只需包含两个外键:CourseID 和 StudentID。 在这种情况下,它将是数据库中没有附加字段(或纯联接表)的多对多联接表。 实体Instructor和Course具有这种多对多关系,下一步是创建一个实体类来充当不包含附加字段的联接表。
EF Core 支持多对多关系的隐式联接表,但本教程尚未更新为使用隐式联接表。 请参阅 “多对多关系”, Razor 即已更新的本教程的 Pages 版本。
CourseAssignment 实体
使用以下代码创建 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; }
}
}
连接实体名称
对于讲师到课程多对多关系,需要在数据库中使用联接表,并且必须由实体集表示。 通常会将联接实体命名为CourseInstructor,在本例中即为EntityName1EntityName2。 但是,我们建议选择描述关系的名称。 数据模型一开始很简单,并会逐渐演变,而不含负载的联接后来常会带上负载。 如果以描述性实体名称开头,则以后无需更改名称。 理想情况下,联接实体在业务领域中应有其自身约定俗成的名称(可能是一个词)。 例如,书籍和客户可以通过评分链接。 对于这种关系,CourseAssignment 比 CourseInstructor 是更好的选择。
复合键
由于外键不可为 null,并且一起唯一标识表的每一行,因此无需单独的主键。
CourseID 和 InstructorID 属性应充当复合主键。 让 EF 识别复合主键的唯一方法是使用 fluent API(不能使用属性完成)。 你将在下一部分中了解如何配置复合主键。
复合键可确保虽然一个课程可以有多行,一个讲师也可以有多行,但同一个讲师和课程不能有多行。
Enrollment联接实体定义其自己的主键,因此可能出现此类重复项。 若要防止此类重复,可以在外键字段上添加唯一索引,或将CourseAssignment配置为类似于Enrollment的主复合键。 有关详细信息,请参阅索引。
更新数据库上下文
将以下突出显示的代码添加到 Data/SchoolContext.cs 该文件:
using ContosoUniversity.Models;
using Microsoft.EntityFrameworkCore;
namespace ContosoUniversity.Data
{
public class SchoolContext : DbContext
{
public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
{
}
public DbSet<Course> Courses { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
public DbSet<CourseAssignment> CourseAssignments { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Course>().ToTable("Course");
modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
modelBuilder.Entity<Student>().ToTable("Student");
modelBuilder.Entity<Department>().ToTable("Department");
modelBuilder.Entity<Instructor>().ToTable("Instructor");
modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
modelBuilder.Entity<CourseAssignment>()
.HasKey(c => new { c.CourseID, c.InstructorID });
}
}
}
此代码添加新实体并配置 CourseAssignment 实体的复合主键。
关于 Fluent API 替代项
类OnModelCreating的DbContext方法中的代码使用Fluent API来配置 EF 行为。 API 称为“fluent”,因为它通常通过将一系列方法调用串联起来组成单个语句,如这个来自 EF Core文档 的示例所示:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Blog>()
.Property(b => b.Url)
.IsRequired();
}
在本教程中,仅对无法使用特性完成的数据库映射使用 Fluent API。 但 Fluent API 还可用于指定大多数格式化、验证和映射规则,这可通过特性完成。
MinimumLength 等特性不能通过 Fluent API 应用。 如前所述,MinimumLength不会更改模式,它仅应用客户端和服务器端验证规则。
某些开发者倾向于仅使用 Fluent API 来让实体类保持“干净”。如有需要,可混合使用特性和 Fluent API,且有些自定义只能通过 Fluent API 实现,但通常建议选择一种方法并尽可能坚持使用这一种。 如果同时使用这两者,请注意,只要有冲突,Fluent API 都会覆盖属性。
有关特性和 Fluent API 的详细信息,请参阅配置方法。
显示关系的实体关系图
下图示展示了 Entity Framework Power Tools 为完整的学校模型创建的关系图。
除了一对多关系线(1 到 *),还可以在此处看到 OfficeAssignment 和 Instructor 实体之间的一对零或一关系线(1 到 0..1),以及讲师和部门实体之间的零或一对多关系线(0..1 到 *)。
使用测试数据初始化数据库
将 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.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();
}
}
}
如第一个教程中所述,此代码中的大多数只是创建新的实体对象,并根据需要将示例数据加载到属性中进行测试。 请注意如何处理多对多关系:代码通过在 CourseAssignment 和 Enrollments 联接实体集中创建实体来创建关系。
添加迁移
保存更改并生成项目。 然后打开项目文件夹中的命令窗口并输入 migrations add 命令(尚未执行 update-database 命令):
dotnet ef migrations add ComplexDataModel
收到有关可能数据丢失的警告。
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 命令(尚未执行此操作),则会出现以下错误:
ALTER TABLE 语句与 FOREIGN KEY 约束“FK_dbo.Course_dbo.Department_DepartmentID”冲突。 冲突发生在数据库“ContosoUniversity”、表“dbo.Department”、列“DepartmentID”。
有时,使用现有数据执行迁移时,需要将存根数据插入数据库中以满足外键约束。 在 Up 方法中生成的代码将不可为 null 的 DepartmentID 外键添加到 Course 表中。 如果代码运行时 Course 表中已有行,则该AddColumn操作会失败,因为 SQL Server 不知道应在该不可为 null 的列中放入什么值。 在本教程中,你将在新数据库上运行迁移,但在生产应用程序中,必须使迁移处理现有数据,因此,以下说明演示了如何执行此操作的示例。
若要使此迁移适用于现有数据,必须更改代码,以便为新列提供默认值,并创建名为“Temp”的存根部门以充当默认部门。 因此,在 Up 方法运行后,现有的 Course 行都将与“Temp”部门相关。
打开
{timestamp}_ComplexDataModel.cs文件。注释掉将 DepartmentID 列添加到 Course 表的代码行。
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);在创建 Department 表的代码后面添加以下突出显示的代码:
migrationBuilder.CreateTable( name: "Department", columns: table => new { DepartmentID = table.Column<int>(nullable: false) .Annotation("SqlServer:ValueGenerationStrategy", SqlServerValueGenerationStrategy.IdentityColumn), Budget = table.Column<decimal>(type: "money", nullable: false), InstructorID = table.Column<int>(nullable: true), Name = table.Column<string>(maxLength: 50, nullable: true), StartDate = table.Column<DateTime>(nullable: false) }, constraints: table => { table.PrimaryKey("PK_Department", x => x.DepartmentID); table.ForeignKey( name: "FK_Department_Instructor_InstructorID", column: x => x.InstructorID, principalTable: "Instructor", principalColumn: "ID", onDelete: ReferentialAction.Restrict); }); migrationBuilder.Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())"); // Default value for FK points to department created above, with // defaultValue changed to 1 in following AddColumn statement. migrationBuilder.AddColumn<int>( name: "DepartmentID", table: "Course", nullable: false, defaultValue: 1);
在生产应用程序中,你将编写代码或脚本来添加部门行并将 Course 行与新的 Department 行相关联。 然后,不再需要“Temp”部门,也不再需要 Course.DepartmentID 列上的默认值。
保存更改并生成项目。
更改连接字符串
现在,DbInitializer 类中现已添加了新代码,用于将新实体的种子数据添加到空数据库中。 若要使 EF 创建新的空数据库,请将 appsettings.json 中的 连接字符串 里的数据库名称更改为 ContosoUniversity3 或你当前使用的计算机上尚未使用的其他名称。
{
"ConnectionStrings": {
"DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
},
将更改保存到 appsettings.json。
Note
作为更改数据库名称的替代方法,可以删除数据库。 使用 SQL Server 对象资源管理器 (SSOX) 或 database drop CLI 命令:
dotnet ef database drop
更新数据库
更改数据库名称或删除数据库后,请在命令窗口中运行 database update 命令以执行迁移。
dotnet ef database update
运行应用以导致 DbInitializer.Initialize 方法运行并填充新数据库。
像之前一样在 SSOX 中打开数据库,然后展开 “表” 节点以查看已创建所有表。 (如果您之前打开的 SSOX 仍处于打开状态,请单击“刷新”按钮。)
运行应用以触发为数据库设定种子的初始化代码。
右键单击 CourseAssignment 表,然后选择“ 查看数据 ”以验证其中是否有数据。
获取代码
后续步骤
在本教程中,你将了解:
- 已自定义数据模型
- 对 Student 实体进行了更改
- 已创建讲师实体
- 已创建 OfficeAssignment 实体
- 修改后的课程实体
- 已创建部门实体
- 修改后的注册登记实体
- 更新了数据库上下文
- 已用测试数据填充数据库
- 已添加迁移
- 更改了连接字符串
- 更新了数据库
转到下一教程,详细了解如何访问相关数据。