首页 > Web开发 > 详细

用ASP.NET Core MVC 和 EF Core 构建Web应用 (五)

时间:2018-06-25 19:58:05      阅读:335      评论:0      收藏:0      [点我收藏+]

之前介绍了由三个实体组成的简单数据模型。 本节将添加更多实体和关系,并通过指定格式化、验证和数据库映射规则来自定义数据模型。

完成本节学习后,实体类将构成下图所示的完整数据模型:

技术分享图片

 

 

使用特性自定义数据模型

介绍如何使用指定格式化、验证和数据库映射规则的特性来自定义数据模型。 随后接着创建完整的学校数据模型,创建方法:向已创建的类添加特性,并为模型中剩余的实体类型创建新类。

DataType 特性

对于学生注册日期,目前所有网页都显示有时间和日期,尽管对此字段而言重要的只是日期。 使用数据注释特性,可更改一次代码,修复每个视图中数据的显示格式。 若要查看如何执行此操作,请向 Student 类的 EnrollmentDate 属性添加一个特性。

在 Models/Student.cs 中,为 System.ComponentModel.DataAnnotations 命名空间添加一个 using 语句,将 DataType 和 DisplayFormat 特性添加到 EnrollmentDate 属性,如以下示例所示:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 
 5 namespace ContosoUniversity.Models
 6 {
 7     public class Student
 8     {
 9         public int ID { get; set; }
10         public string LastName { get; set; }
11         public string FirstMidName { get; set; }
12         [DataType(DataType.Date)]
13         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
14         public DateTime EnrollmentDate { get; set; }
15 
16         public ICollection<Enrollment> Enrollments { get; set; }
17     }
18 }

DataType 属性用于指定比数据库内部类型更具体的数据类型。 在此示例中,我们只想跟踪日期,而不是日期和时间。 DataType 枚举提供了多种数据类型,例如日期、时间、电话号码、货币、电子邮件地址等。 应用程序还可通过 DataType 特性自动提供类型特定的功能。 例如,可以为 DataType.EmailAddress 创建 mailto:链接,并且可以在支持 HTML5 的浏览器中为 DataType.Date 提供日期选择器。 DataType 特性发出 HTML 5 data-(读作 data dash)特性供 HTML 5 浏览器理解。 DataType 特性不提供任何验证。

DataType.Date 不指定显示日期的格式。 默认情况下,数据字段根据默认格式显示,后者以服务器的 CultureInfo 为基础。

DisplayFormat 特性用于显式指定日期格式。

ApplyFormatInEditMode 设置指定在文本框中显示值以进行编辑时也应用格式。 (你可能不想为某些字段执行此操作 — 例如,对于货币值,你可能不希望文本框中的货币符号可编辑。)

可单独使用 DisplayFormat 特性,但通常建议同时使用 DataType 特性。 DataType 特性传达数据的语义而不是传达数据在屏幕上的呈现方式,并提供 DisplayFormat 不具备的以下优势:

  • 浏览器可启用 HTML5 功能(例如,显示日历控件、区域设置适用的货币符号、电子邮件链接、某种客户端输入验证等)。

  • 默认情况下,浏览器将根据区域设置采用正确的格式呈现数据。

运行应用,进入“学生”索引页,注意注册日期中不再显示时间。 任何使用“学生”模型的视图都是如此。

技术分享图片

 

StringLength 特性

还可使用特性指定数据验证规则和验证错误消息。 StringLength 特性设置数据库中的最大长度,并为 ASP.NET MVC 提供客户端和服务器端验证。 还可在此属性中指定最小字符串长度,但最小值对数据库架构没有影响。

假设要确保用户输入的名称不超过 50 个字符。 若要添加此限制,请将 StringLength 特性添加到 LastName和 FirstMidName 属性,如下例所示:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 
 5 namespace ContosoUniversity.Models
 6 {
 7     public class Student
 8     {
 9         public int ID { get; set; }
10         [StringLength(50)]
11         public string LastName { get; set; }
12         [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
13         public string FirstMidName { get; set; }
14         [DataType(DataType.Date)]
15         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
16         public DateTime EnrollmentDate { get; set; }
17 
18         public ICollection<Enrollment> Enrollments { get; set; }
19     }
20 }

StringLength 特性不会阻止用户在名称中输入空格。 可使用 RegularExpression 特性应用输入限制。 例如,以下代码要求第一个字符为大写,其余字符按字母顺序排列:

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

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 特性指定,创建数据库时,映射到 FirstMidName 属性的 Student 表的列将被命名为 FirstName换言之,在代码引用 Student.FirstMidName 时,数据来源将是 Student 表的 FirstName 列或在其中进行更新。 如果不指定列名称,则其名称与属性名称相同。

在 Student.cs 文件中,为 System.ComponentModel.DataAnnotations.Schema 添加一个 using 语句,并将列名称特性添加到 FirstMidName 属性,如以下突出显示的代码所示:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Student
 9     {
10         public int ID { get; set; }
11         [StringLength(50)]
12         public string LastName { get; set; }
13         [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
14         [Column("FirstName")]
15         public string FirstMidName { get; set; }
16         [DataType(DataType.Date)]
17         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
18         public DateTime EnrollmentDate { get; set; }
19 
20         public ICollection<Enrollment> Enrollments { get; set; }
21     }
22 }

添加 Column 特性后,SchoolContext 的支持模型会发生改变,与数据库不再匹配。

保存更改并生成项目。 随后打开项目文件夹中的命令窗口,输入以下命令,创建另一个迁移:

dotnet ef migrations add ColumnFirstName

dotnet ef database update

在 SQL Server 对象资源管理器中,双击 Student 表,打开 Student 表设计器。

技术分享图片

 

在进行前两次迁移前,名称列的类型为 nvarchar (MAX)。 而现在则是 nvarchar (50),列名从 FirstMidName 变为 FirstName。

 

Student 实体的最终更改

在 Models/Student.cs 中,将之前添加的代码替换为以下代码。 突出显示所作更改。

 

 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Student
 9     {
10         public int ID { get; set; }
11         [Required]
12         [StringLength(50)]
13         [Display(Name = "Last Name")]
14         public string LastName { get; set; }
15         [Required]
16         [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
17         [Column("FirstName")]
18         [Display(Name = "First Name")]
19         public string FirstMidName { get; set; }
20         [DataType(DataType.Date)]
21         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
22         [Display(Name = "Enrollment Date")]
23         public DateTime EnrollmentDate { get; set; }
24         [Display(Name = "Full Name")]
25         public string FullName
26         {
27             get
28             {
29                 return LastName + ", " + FirstMidName;
30             }
31         }
32 
33         public ICollection<Enrollment> Enrollments { get; set; }
34     }
35 }

Required 特性

Required 特性使名称属性成为必填字段。 值类型(DateTime、int、double、float 等)等不可为 null 的类型不需要 Required 特性。 系统会将不可为 null 的类型自动视为必填字段。

可删除 Required 特性,并用 StringLength 特性的最小长度参数来替换:

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

Display 特性

Display 特性指定文本框的标题应是“名”、“姓”、“全名”和“注册日期”,而不是每个实例中的属性名称(其中没有分隔单词的空格)。

FullName 计算属性

FullName 是计算属性,可返回通过串联两个其他属性创建的值。 因此它只有一个 get 访问器,且数据库中不会生成 FullName 列。

 

创建 Instructor 实体

创建 Models/Instructor.cs,使用以下代码替换模板代码:

技术分享图片
 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Instructor
 9     {
10         public int ID { get; set; }
11 
12         [Required]
13         [Display(Name = "Last Name")]
14         [StringLength(50)]
15         public string LastName { get; set; }
16 
17         [Required]
18         [Column("FirstName")]
19         [Display(Name = "First Name")]
20         [StringLength(50)]
21         public string FirstMidName { get; set; }
22 
23         [DataType(DataType.Date)]
24         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
25         [Display(Name = "Hire Date")]
26         public DateTime HireDate { get; set; }
27 
28         [Display(Name = "Full Name")]
29         public string FullName
30         {
31             get { return LastName + ", " + FirstMidName; }
32         }
33 
34         public ICollection<CourseAssignment> CourseAssignments { get; set; }
35         public OfficeAssignment OfficeAssignment { get; set; }
36     }
37 }
View Code

注意,在 Student 和 Instructor 实体中有几个属性是相同的。

可在一行中放置多个特性,因此也可以按如下所示编写 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:

技术分享图片
 1 using System.ComponentModel.DataAnnotations;
 2 using System.ComponentModel.DataAnnotations.Schema;
 3 
 4 namespace ContosoUniversity.Models
 5 {
 6     public class OfficeAssignment
 7     {
 8         [Key]
 9         public int InstructorID { get; set; }
10         [StringLength(50)]
11         [Display(Name = "Office Location")]
12         public string Location { get; set; }
13 
14         public Instructor Instructor { get; set; }
15     }
16 }
View Code

Key 特性

讲师与 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)。 Instructor 实体具有相关 OfficeAssignment 实体时,每个实体都将在其导航属性中引用另一实体。

可向 Instructor 导航属性添加 [Required] 特性,指定必须有相关讲师,但这不是必须的,因为 InstructorID外键(也是此表的键)不可为 null。

 

修改 Course 实体

在 Models/Course.cs 中,将之前添加的代码替换为以下代码。 突出显示所作更改。

 

 1 using System.Collections.Generic;
 2 using System.ComponentModel.DataAnnotations;
 3 using System.ComponentModel.DataAnnotations.Schema;
 4 
 5 namespace ContosoUniversity.Models
 6 {
 7     public class Course
 8     {
 9         [DatabaseGenerated(DatabaseGeneratedOption.None)]
10         [Display(Name = "Number")]
11         public int CourseID { get; set; }
12 
13         [StringLength(50, MinimumLength = 3)]
14         public string Title { get; set; }
15 
16         [Range(0, 5)]
17         public int Credits { get; set; }
18 
19         public int DepartmentID { get; set; }
20 
21         public Department Department { get; set; }
22         public ICollection<Enrollment> Enrollments { get; set; }
23         public ICollection<CourseAssignment> CourseAssignments { get; set; }
24     }
25 }

Course 实体具有外键属性 DepartmentID,该属性指向相关 Department 实体,同时它还具有 Department 导航属性。

如果拥有相关实体的导航属性,则 Entity Framework 不会要求为数据模型添加外键属性。 只要有需要,EF 就会自动在数据库中创建外键,并为其创建阴影属性。 但如果数据模型包含外键,则更新会变得更简单、更高效。例如,提取 Course 实体进行编辑时,如果未加载该实体,那么 Department 实体为 null,因此,更新 Course 实体时,必须先提取 Department 实体。 数据模型中包含外键属性 DepartmentID 时,更新前无需提取 Department 实体。

DatabaseGenerated 特性

CourseID 属性上具有 None 参数的 DatabaseGenerated 特性指定主键值由用户提供,而不是由数据库生成。

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

 

创建 Department 实体

用以下代码创建 Models/Department.cs:

技术分享图片
 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class Department
 9     {
10         public int DepartmentID { get; set; }
11 
12         [StringLength(50, MinimumLength = 3)]
13         public string Name { get; set; }
14 
15         [DataType(DataType.Currency)]
16         [Column(TypeName = "money")]
17         public decimal Budget { get; set; }
18 
19         [DataType(DataType.Date)]
20         [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
21         [Display(Name = "Start Date")]
22         public DateTime StartDate { get; set; }
23 
24         public int? InstructorID { get; set; }
25 
26         public Instructor Administrator { get; set; }
27         public ICollection<Course> Courses { get; set; }
28     }
29 }
View Code

Column 特性

Column 特性之前用于更改列名称映射。 在 Department 实体的代码中,Column 特性用于更改 SQL 数据类型映射,以便使用数据库中的 SQL Server 货币类型来定义该列:

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

通常不需要列映射,因为 Entity Framework 会根据你为属性定义的 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; }

备注按照约定,Entity Framework 能针对不可为 null 的外键和多对多关系启用级联删除。 这可能导致循环级联删除规则,尝试添加迁移时该规则会造成异常。 例如,如果未将 Department.InstructorID 属性定义为可为 null,那么在删除系时,EF 会配置级联删除规则来删除讲师,这是预期外的情况。 如果业务规则要求 InstructorID 属性不可为 null,则必须使用以下 Fluent API 语句禁用关系中的级联删除:

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

 

修改 Enrollment 实体

在 Models/Enrollment.cs 中,将之前添加的代码替换为以下代码:

 1 using System.ComponentModel.DataAnnotations;
 2 using System.ComponentModel.DataAnnotations.Schema;
 3 
 4 namespace ContosoUniversity.Models
 5 {
 6     public enum Grade
 7     {
 8         A, B, C, D, F
 9     }
10 
11     public class Enrollment
12     {
13         public int EnrollmentID { get; set; }
14         public int CourseID { get; set; }
15         public int StudentID { get; set; }
16         [DisplayFormat(NullDisplayText = "No grade")]
17         public Grade? Grade { get; set; }
18 
19         public Course Course { get; set; }
20         public Student Student { get; set; }
21     }
22 }

外键和导航属性

外键属性和导航属性可反映以下关系:

注册记录面向一门课程,因此存在 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 属性)。

下图显示这些关系在实体关系图中的外观。

技术分享图片

 

每条关系线的一端显示 1,另一端显示星号 (*),这表示一对多关系。

如果 Enrollment 表不包含年级信息,则它只需包含两个外键(CourseID 和 StudentID)。 在这种情况下,该表是数据库中不带有效负载的多对多联接表(或纯联接表)。 Instructor 和 Course 实体具有这种多对多关系,下一步是创建实体类,将其作为不带有效负载的联接表。(EF 6.x 支持多对多关系的隐式联接表,但 EF Core 不支持。)

CourseAssignment 实体

用以下代码创建 Models/CourseAssignment.cs:

技术分享图片
 1 using System;
 2 using System.Collections.Generic;
 3 using System.ComponentModel.DataAnnotations;
 4 using System.ComponentModel.DataAnnotations.Schema;
 5 
 6 namespace ContosoUniversity.Models
 7 {
 8     public class CourseAssignment
 9     {
10         public int InstructorID { get; set; }
11         public int CourseID { get; set; }
12         public Instructor Instructor { get; set; }
13         public Course Course { get; set; }
14     }
15 }
View Code

联接实体名称

数据库中的 Instructor 与 Course 多对多关系需要联接表,并且必须由实体集表示。 将联接实体命名为 EntityName1EntityName2 很常见,在此示例中实体名为 CourseInstructor。 但是,建议选择一个可描述关系的名称。 数据模型开始很简单,且会不断增长,随后无有效负载联接会频繁获取有效负载。 如果一开始实体名称为描述性名称,那么之后就不必更改名称。 理想情况下,联接实体在业务域中可能具有自己的自带名称(可能是单个字)。 例如,可通过 Rating 链接 Book 和 Customer。 对于此关系,相比 CourseInstructorCourseAssignment 是更好的选择。

组合键

由于外键不可为 null,且它们共同唯一标识表的每一行,因此不需要单独的主键。 InstructorID 和 CourseID 属性应充当组合主键。 标识 EF 组合主键的唯一方法是使用 Fluent API(无法借助特性来完成)。 下一节将介绍如何配置组合主键。

在一个课程可以有多个行,一个讲师可以有多个行的情况下,组合键可确保同一讲师和课程不会有多个行。 Enrollment 联接实体定义其自己的主键,因此可能会出现此类重复。 若要防止出现此类重复,可在外键字段上添加唯一索引,或使用类似于 CourseAssignment 的主组合键配置 Enrollment

 

更新数据库上下文

将以下突出显示的代码添加到 Data/SchoolContext.cs 文件:

 1 using ContosoUniversity.Models;
 2 using Microsoft.EntityFrameworkCore;
 3 
 4 namespace ContosoUniversity.Data
 5 {
 6     public class SchoolContext : DbContext
 7     {
 8         public SchoolContext(DbContextOptions<SchoolContext> options) : base(options)
 9         {
10         }
11 
12         public DbSet<Course> Courses { get; set; }
13         public DbSet<Enrollment> Enrollments { get; set; }
14         public DbSet<Student> Students { get; set; }
15         public DbSet<Department> Departments { get; set; }
16         public DbSet<Instructor> Instructors { get; set; }
17         public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
18         public DbSet<CourseAssignment> CourseAssignments { get; set; }
19 
20         protected override void OnModelCreating(ModelBuilder modelBuilder)
21         {
22             modelBuilder.Entity<Course>().ToTable("Course");
23             modelBuilder.Entity<Enrollment>().ToTable("Enrollment");
24             modelBuilder.Entity<Student>().ToTable("Student");
25             modelBuilder.Entity<Department>().ToTable("Department");
26             modelBuilder.Entity<Instructor>().ToTable("Instructor");
27             modelBuilder.Entity<OfficeAssignment>().ToTable("OfficeAssignment");
28             modelBuilder.Entity<CourseAssignment>().ToTable("CourseAssignment");
29 
30             modelBuilder.Entity<CourseAssignment>()
31                 .HasKey(c => new { c.CourseID, c.InstructorID });
32         }
33     }
34 }

此代码添加新实体并配置 CourseAssignment 实体的组合主键。

用 Fluent API 替代特性

DbContext 类的 OnModelCreating 方法中的代码使用 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 就会覆盖特性。

显示关系的实体关系图

下图显示 Entity Framework Power Tools 针对已完成的学校模型创建的关系图。

技术分享图片

 

除一对多关系线(1到 *)外,此处还显示了 Instructor 和 OfficeAssignment 实体间的一对零或一关系线(1 到 0..1),以及 Instructor 和 Department 实体间的零对多或一对多关系线(0..1 到 *)。

 

使用测试数据设定数据库种子

使用以下代码替换 Data/DbInitializer.cs 文件中的代码,从而为创建的新实体提供种子数据。

技术分享图片
  1 using System;
  2 using System.Linq;
  3 using Microsoft.EntityFrameworkCore;
  4 using Microsoft.Extensions.DependencyInjection;
  5 using ContosoUniversity.Models;
  6 
  7 namespace ContosoUniversity.Data
  8 {
  9     public static class DbInitializer
 10     {
 11         public static void Initialize(SchoolContext context)
 12         {
 13             //context.Database.EnsureCreated();
 14 
 15             // Look for any students.
 16             if (context.Students.Any())
 17             {
 18                 return;   // DB has been seeded
 19             }
 20 
 21             var students = new Student[]
 22             {
 23                 new Student { FirstMidName = "Carson",   LastName = "Alexander",
 24                     EnrollmentDate = DateTime.Parse("2010-09-01") },
 25                 new Student { FirstMidName = "Meredith", LastName = "Alonso",
 26                     EnrollmentDate = DateTime.Parse("2012-09-01") },
 27                 new Student { FirstMidName = "Arturo",   LastName = "Anand",
 28                     EnrollmentDate = DateTime.Parse("2013-09-01") },
 29                 new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
 30                     EnrollmentDate = DateTime.Parse("2012-09-01") },
 31                 new Student { FirstMidName = "Yan",      LastName = "Li",
 32                     EnrollmentDate = DateTime.Parse("2012-09-01") },
 33                 new Student { FirstMidName = "Peggy",    LastName = "Justice",
 34                     EnrollmentDate = DateTime.Parse("2011-09-01") },
 35                 new Student { FirstMidName = "Laura",    LastName = "Norman",
 36                     EnrollmentDate = DateTime.Parse("2013-09-01") },
 37                 new Student { FirstMidName = "Nino",     LastName = "Olivetto",
 38                     EnrollmentDate = DateTime.Parse("2005-09-01") }
 39             };
 40 
 41             foreach (Student s in students)
 42             {
 43                 context.Students.Add(s);
 44             }
 45             context.SaveChanges();
 46 
 47             var instructors = new Instructor[]
 48             {
 49                 new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
 50                     HireDate = DateTime.Parse("1995-03-11") },
 51                 new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
 52                     HireDate = DateTime.Parse("2002-07-06") },
 53                 new Instructor { FirstMidName = "Roger",   LastName = "Harui",
 54                     HireDate = DateTime.Parse("1998-07-01") },
 55                 new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
 56                     HireDate = DateTime.Parse("2001-01-15") },
 57                 new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
 58                     HireDate = DateTime.Parse("2004-02-12") }
 59             };
 60 
 61             foreach (Instructor i in instructors)
 62             {
 63                 context.Instructors.Add(i);
 64             }
 65             context.SaveChanges();
 66 
 67             var departments = new Department[]
 68             {
 69                 new Department { Name = "English",     Budget = 350000,
 70                     StartDate = DateTime.Parse("2007-09-01"),
 71                     InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
 72                 new Department { Name = "Mathematics", Budget = 100000,
 73                     StartDate = DateTime.Parse("2007-09-01"),
 74                     InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
 75                 new Department { Name = "Engineering", Budget = 350000,
 76                     StartDate = DateTime.Parse("2007-09-01"),
 77                     InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
 78                 new Department { Name = "Economics",   Budget = 100000,
 79                     StartDate = DateTime.Parse("2007-09-01"),
 80                     InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
 81             };
 82 
 83             foreach (Department d in departments)
 84             {
 85                 context.Departments.Add(d);
 86             }
 87             context.SaveChanges();
 88 
 89             var courses = new Course[]
 90             {
 91                 new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
 92                     DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID
 93                 },
 94                 new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
 95                     DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
 96                 },
 97                 new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
 98                     DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID
 99                 },
100                 new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
101                     DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
102                 },
103                 new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
104                     DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID
105                 },
106                 new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
107                     DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
108                 },
109                 new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
110                     DepartmentID = departments.Single( s => s.Name == "English").DepartmentID
111                 },
112             };
113 
114             foreach (Course c in courses)
115             {
116                 context.Courses.Add(c);
117             }
118             context.SaveChanges();
119 
120             var officeAssignments = new OfficeAssignment[]
121             {
122                 new OfficeAssignment {
123                     InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
124                     Location = "Smith 17" },
125                 new OfficeAssignment {
126                     InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
127                     Location = "Gowan 27" },
128                 new OfficeAssignment {
129                     InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
130                     Location = "Thompson 304" },
131             };
132 
133             foreach (OfficeAssignment o in officeAssignments)
134             {
135                 context.OfficeAssignments.Add(o);
136             }
137             context.SaveChanges();
138 
139             var courseInstructors = new CourseAssignment[]
140             {
141                 new CourseAssignment {
142                     CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
143                     InstructorID = instructors.Single(i => i.LastName == "Kapoor").ID
144                     },
145                 new CourseAssignment {
146                     CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
147                     InstructorID = instructors.Single(i => i.LastName == "Harui").ID
148                     },
149                 new CourseAssignment {
150                     CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
151                     InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
152                     },
153                 new CourseAssignment {
154                     CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
155                     InstructorID = instructors.Single(i => i.LastName == "Zheng").ID
156                     },
157                 new CourseAssignment {
158                     CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
159                     InstructorID = instructors.Single(i => i.LastName == "Fakhouri").ID
160                     },
161                 new CourseAssignment {
162                     CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
163                     InstructorID = instructors.Single(i => i.LastName == "Harui").ID
164                     },
165                 new CourseAssignment {
166                     CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
167                     InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
168                     },
169                 new CourseAssignment {
170                     CourseID = courses.Single(c => c.Title == "Literature" ).CourseID,
171                     InstructorID = instructors.Single(i => i.LastName == "Abercrombie").ID
172                     },
173             };
174 
175             foreach (CourseAssignment ci in courseInstructors)
176             {
177                 context.CourseAssignments.Add(ci);
178             }
179             context.SaveChanges();
180 
181             var enrollments = new Enrollment[]
182             {
183                 new Enrollment {
184                     StudentID = students.Single(s => s.LastName == "Alexander").ID,
185                     CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
186                     Grade = Grade.A
187                 },
188                     new Enrollment {
189                     StudentID = students.Single(s => s.LastName == "Alexander").ID,
190                     CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
191                     Grade = Grade.C
192                     },
193                     new Enrollment {
194                     StudentID = students.Single(s => s.LastName == "Alexander").ID,
195                     CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
196                     Grade = Grade.B
197                     },
198                     new Enrollment {
199                         StudentID = students.Single(s => s.LastName == "Alonso").ID,
200                     CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
201                     Grade = Grade.B
202                     },
203                     new Enrollment {
204                         StudentID = students.Single(s => s.LastName == "Alonso").ID,
205                     CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
206                     Grade = Grade.B
207                     },
208                     new Enrollment {
209                     StudentID = students.Single(s => s.LastName == "Alonso").ID,
210                     CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
211                     Grade = Grade.B
212                     },
213                     new Enrollment {
214                     StudentID = students.Single(s => s.LastName == "Anand").ID,
215                     CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
216                     },
217                     new Enrollment {
218                     StudentID = students.Single(s => s.LastName == "Anand").ID,
219                     CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
220                     Grade = Grade.B
221                     },
222                 new Enrollment {
223                     StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
224                     CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
225                     Grade = Grade.B
226                     },
227                     new Enrollment {
228                     StudentID = students.Single(s => s.LastName == "Li").ID,
229                     CourseID = courses.Single(c => c.Title == "Composition").CourseID,
230                     Grade = Grade.B
231                     },
232                     new Enrollment {
233                     StudentID = students.Single(s => s.LastName == "Justice").ID,
234                     CourseID = courses.Single(c => c.Title == "Literature").CourseID,
235                     Grade = Grade.B
236                     }
237             };
238 
239             foreach (Enrollment e in enrollments)
240             {
241                 var enrollmentInDataBase = context.Enrollments.Where(
242                     s =>
243                             s.Student.ID == e.StudentID &&
244                             s.Course.CourseID == e.CourseID).SingleOrDefault();
245                 if (enrollmentInDataBase == null)
246                 {
247                     context.Enrollments.Add(e);
248                 }
249             }
250             context.SaveChanges();
251         }
252     }
253 }
View Code

大部分此类代码仅创建新实体对象,并按测试要求将示例数据加载到属性中。 注意多对多关系的处理方法:代码在 Enrollments 和 CourseAssignment 联接实体集中创建实体,以此来创建关系。

添加迁移

保存更改并生成项目。 然后打开项目文件夹中的命令窗口,输入 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”的存根系,作为默认系。之后,Course 行将在 Up 方法运行后与“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);

在生产应用程序中,可编写代码或脚本来添加 Department 行并将 Course 行与新 Department 行相关联。 随后将不在需要“Temp”系或 Course.DepartmentID 列中的默认值。

保存更改并生成项目。

 

更改连接字符串并更新数据库

 现在 DbInitializer 类中就有了新代码,可将新实体的种子数据添加到空数据库。 若要让 EF 创建新的空数据库,请将 appsettings.json 中连接字符串内的数据库名称更改为 ContosoUniversity3 或正在使用的计算机上未使用过的其他名称。

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=ContosoUniversity3;Trusted_Connection=True;MultipleActiveResultSets=true"
  },

将更改保存到 appsettings.json。

备注除更改数据库名称外,删除数据库同样可行。 使用 SQL Server 对象资源管理器 (SSOX) 或 database dropCLI 命令: dotnet ef database drop

更改数据库名称或删除数据库后,在命令窗口运行 database update 命令,执行迁移。

dotnet ef database update

运行应用,使 DbInitializer.Initialize 方法运行并填充新数据库。

像之前一样在 SSOX 中打开数据库,然后展开 Tables 节点,查看是否已创建所有表。 (如果之前打开的 SSOX 尚未关闭,请单击“刷新”按钮。)

技术分享图片

运行应用,触发设定数据库种子的初始化代码。

右键单击“CourseAssignment”表,然后选择“查看数据”,验证其中是否存在数据。

技术分享图片

 

 

总结

现在你就得到了更复杂的数据模型和相应的数据库。 

 

 

 

 

 *****************************
 *** Keep learning and growing. ***
 *****************************

用ASP.NET Core MVC 和 EF Core 构建Web应用 (五)

原文:https://www.cnblogs.com/gangle/p/9225813.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!