在前面的教程中,你使用了由三个实体组成的简单数据模型。 在本教程中,您将添加更多实体和关系,并通过指定格式设置、验证和数据库映射规则来自定义数据模型。 本文介绍了两种自定义数据模型的方法:通过向实体类添加属性和向数据库上下文类添加代码。
完成本教程后,实体类将构成下图所示的完整数据模型:
在本教程中,你将了解:
- 自定义数据模型
- 更新学生实体
- 创建 Instructor 实体
- 创建 OfficeAssignment 实体
- 修改课程实体
- 创建 Department 实体
- 修改 Enrollment 实体
- 将代码添加到数据库上下文
- 使用测试数据设定数据库种子
- 添加迁移
- 更新数据库
一、系统必备
二、自定义数据模型
本节介绍如何使用指定格式化、验证和数据库映射规则的特性来自定义数据模型。 然后,在以下几个部分中,您将通过向已创建的类添加特性并为模型中的其余实体类型创建新类,来创建完整的 School
数据模型。
1、DataType 特性
对于学生注册日期,目前所有网页都显示有时间和日期,尽管对此字段而言重要的只是日期。 使用数据注释特性,可更改一次代码,修复每个视图中数据的显示格式。 若要查看如何执行此操作,请向 EnrollmentDate
类的 Student
属性添加一个特性。
在ModelsStudent.cs中,添加 System.ComponentModel.DataAnnotations
命名空间的 using
语句,并将 DataType
和 DisplayFormat
特性添加到 EnrollmentDate
属性,如以下示例中所示:
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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
DataType特性用于指定比数据库内部类型更具体的数据类型。 在此示例中,我们只想跟踪日期,而不是日期和时间。 DataType 枚举提供多种数据类型,例如日期、时间、PhoneNumber、货币、EmailAddress等。 应用程序还可通过 DataType
特性自动提供类型特定的功能。 例如,可以为EmailAddress创建 mailto:
链接,并且可以在支持HTML5的浏览器中为数据类型提供日期选择器。 数据类型属性发出 html 5数据(发音为数据破折号)属性,html 5 浏览器可以理解这些属性。 DataType特性不提供任何验证。
DataType.Date
不指定显示日期的格式。 默认情况下,数据字段根据服务器的CultureInfo按默认格式显示。
DisplayFormat
特性用于显式指定日期格式:
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
ApplyFormatInEditMode
设置指定在文本框中显示值进行编辑时,还应应用指定的格式设置。 (您可能不希望为某些字段(例如,对于货币值),您可能不希望文本框中的货币符号进行编辑。)
您可以单独使用DisplayFormat属性,但通常最好使用DataType属性。 DataType
属性传达数据的语义,而不是将其呈现在屏幕上,并且提供了 DisplayFormat
不会获得的以下好处:
- 浏览器可启用 HTML5 功能(例如,显示日历控件、区域设置适用的货币符号、电子邮件链接、某种客户端输入验证等)。
- 默认情况下,浏览器将根据区域设置使用正确的格式呈现数据。
- DataType特性可以使 MVC 选择正确的字段模板来呈现数据( DisplayFormat使用字符串模板)。 有关详细信息,请参阅 Brad Wilson 的ASP.NET MVC 2 模板。 (尽管是针对 MVC 2 编写的,但本文仍适用于当前版本的 ASP.NET MVC。)
如果将 DataType
特性与日期字段结合使用,则还必须指定 DisplayFormat
属性,以确保字段在 Chrome 浏览器中正确呈现。 有关详细信息,请参阅此 StackOverflow 线程。
有关如何在 MVC 中处理其他日期格式的详细信息,请参阅mvc 5 简介:检查编辑方法和编辑视图,并在页面中搜索 "国际化"。
再次运行 "学生索引" 页,注意注册日期不再显示时间。 对于使用 Student
模型的任何视图,这一点都是相同的。
2、StringLengthAttribute
还可使用特性指定数据验证规则和验证错误消息。 StringLength 特性设置数据库中的最大长度,并为 ASP.NET 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, 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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
StringLength属性不会阻止用户为某个名称输入空格。 可以使用RegularExpression属性将限制应用到输入。 例如,下面的代码要求第一个字符为大写,其余的字符为字母顺序:
[RegularExpression(@"^[A-Z]+[a-zA-Z""'s-]*$")]
MaxLength特性为StringLength特性提供了类似的功能,但不提供客户端验证。
运行应用程序,然后单击 "学生" 选项卡。收到以下错误:
创建数据库后,支持 "SchoolContext" 上下文的模型已发生更改。请考虑使用 Code First 迁移更新数据库(https://go.microsoft.com/fwlink/?LinkId=238269)。
数据库模型已更改,这种情况下需要更改数据库架构,并且实体框架检测到的情况。 你将使用迁移来更新架构,而不会丢失你使用 UI 添加到数据库的任何数据。 如果更改了由 Seed
方法创建的数据,则将更改回其原始状态,因为在 Seed
方法中使用了AddOrUpdate方法。 (AddOrUpdate相当于数据库术语中的 "upsert" 操作。)
在包管理器控制台 (PMC) 中输入以下命令:
add-migration MaxLengthOnNames
update-database
add-migration
命令将创建一个名为 <timeStamp>_MaxLengthOnNames.cs的文件。 此文件包含 Up
方法中的代码,该代码将更新数据库以匹配当前数据模型。 update-database
命令运行该代码。
实体框架使用迁移文件名前面预置的时间戳来对迁移进行排序。 你可以在运行 update-database
命令之前创建多个迁移,然后按创建顺序应用所有迁移。
运行 "创建" 页,并输入长度超过50个字符的名称。 单击 "创建" 时,客户端验证会显示一条错误消息: "LastName" 字段必须是最大长度为50的字符串。
3、列属性
还可使用特性来控制类和属性映射到数据库的方式。 假设在名字字段使用了 FirstMidName
,这是因为该字段也可能包含中间名。 但却希望将数据库列命名为 FirstName
,因为要针对数据库编写即席查询的用户习惯使用该姓名。 若要进行此映射,可使用 Column
特性。
Column
特性指定,创建数据库时,映射到 Student
属性的 FirstMidName
表的列将被命名为 FirstName
。 换言之,在代码引用 Student.FirstMidName
时,数据来源将是 FirstName
表的 Student
列或在其中进行更新。 如果未指定列名,则将其指定为与属性名称相同的名称。
在Student.cs文件中,添加system.componentmodel的 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, 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 virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
添加列属性会更改 SchoolContext 的模型,因此它不会与数据库匹配。 在 PMC 中输入以下命令以创建另一个迁移:
add-migration ColumnFirstName
update-database
在服务器资源管理器中,双击student表,打开student表设计器。
下图显示了在应用前两个迁移之前的原始列名称。 除了从 FirstMidName
更改为 FirstName
的列名以外,两个名称列已从 MAX
长度更改为50个字符。
你还可以使用熟知的 API进行数据库映射更改,如本教程的后面部分所示。
Note
如果尚未按以下各节所述创建所有实体类就尝试进行编译,则可能会出现编译器错误。
三、更新学生实体
在ModelsStudent.cs中,将之前添加的代码替换为以下代码。 突出显示所作更改。
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[StringLength(50)]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
[Column("FirstName")]
[Display(Name = "First Name")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
[Display(Name = "Enrollment Date")]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection<Enrollment> Enrollments { get; set; }
}
}
1、必需的属性
必需的属性使名称属性成为必填字段。 值类型(例如 DateTime、int、double 和 float)不需要 Required attribute
。 值类型不能赋予 null 值,因此它们原本被视为必填字段。
Required
特性必须与 MinimumLength
结合使用才能强制执行 MinimumLength
。
[Display(Name = "Last Name")]
[Required]
[StringLength(50, MinimumLength=2)]
public string LastName { get; set; }
MinimumLength
和 Required
允许通过空格来满足验证。 对字符串使用完整控制的 RegularExpression
属性。
2、显示属性
Display
特性指定文本框的标题应是“名”、“姓”、“全名”和“注册日期”,而不是每个实例中的属性名称(其中没有分隔单词的空格)。
3、FullName 计算属性
FullName
是计算属性,可返回通过串联两个其他属性创建的值。 因此,它只有一个 get
访问器,且在数据库中不会生成任何 FullName
列。
四、创建 Instructor 实体
创建ModelsInstructor.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 virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
}
请注意,Student
和 Instructor
实体中具有几个相同属性。 本系列后面的实现继承教程将重构此代码以消除冗余。
可以将多个属性放在一行上,因此还可以编写讲师类,如下所示:
public class Instructor
{
public int ID { get; set; }
[Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
[Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
public string FirstMidName { get; set; }
[DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection<Course> Courses { get; set; }
public virtual OfficeAssignment OfficeAssignment { get; set; }
}
1、课程和 OfficeAssignment 导航属性
Courses
和 OfficeAssignment
是导航属性。 如前所述,它们通常定义为虚拟,以便能够利用称为延迟加载的实体框架功能。 此外,如果导航属性可以包含多个实体,则其类型必须实现ICollection<t>接口。 例如, IList<t>限定,而不是IEnumerable<t> ,因为 IEnumerable<T>
不实现Add。
讲师可以讲授任意数量的课程,因此 Courses
定义为 Course
实体的集合。
public virtual ICollection<Course> Courses { get; set; }
我们的业务规则陈述,一个指导员最多只能有一个办公室,因此 OfficeAssignment
定义为单个 OfficeAssignment
实体(如果没有分配任何 office,可能 null
此实体)。
public virtual OfficeAssignment OfficeAssignment { get; set; }
五、创建 OfficeAssignment 实体
用以下代码创建ModelsOfficeAssignment.cs :
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class OfficeAssignment
{
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
[StringLength(50)]
[Display(Name = "Office Location")]
public string Location { get; set; }
public virtual Instructor Instructor { get; set; }
}
}
生成项目,该项目保存更改并验证编译器能否捕获任何复制和粘贴错误。
1、键属性
Instructor
和 OfficeAssignment
实体之间存在一对零或一关系。 Office 分配只与分配给它的指导员相关,因此,其主键也是 Instructor
实体的外键。 但实体框架无法自动将 InstructorID
视为此实体的主键,因为其名称不遵循 ID
或classname ID
命名约定。 因此,Key
特性用于将其识别为主键:
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
如果实体具有其自己的主键,但你想要将属性命名为不同于 classnameID
或 ID
的名称,也可以使用 Key
特性。 默认情况下,EF 将该键视为非数据库生成,因为列是用于标识关系的。
2、ForeignKey 特性
如果两个实体之间存在一对零或一关系或一对一关系(如 OfficeAssignment
与 Instructor
之间),则 EF 无法确定关系的哪一端是主体,哪一端依赖于。 一对一关系在每个类中都有一个指向另一个类的引用导航属性。 ForeignKey 特性可应用于依赖类以建立关系。 如果省略了ForeignKey 属性,则在尝试创建迁移时,会收到以下错误:
无法确定类型为 "ContosoUniversity" 和 "ContosoUniversity" 之间的关联的主体端。必须使用关系 Fluent API 或数据批注显式配置此关联的主体端。
稍后在本教程中,你将了解如何配置此关系与 Fluent API。
3、讲师导航属性
Instructor
实体有一个可以为 null 的 OfficeAssignment
导航属性(因为讲师可能没有办公室分配),并且 OfficeAssignment
实体具有不可为 null 的 Instructor
导航属性(因为在没有讲师的情况下,不能存在办公室分配,InstructorID
不可以为 null)。 当 Instructor
实体具有相关的 OfficeAssignment
实体时,每个实体在其导航属性中都具有对其他实体的引用。
您可以在讲师导航属性中放置一个 [Required]
特性,以指定必须有相关的指导员,但您不必这样做,因为 InstructorID 外键(也是此表的键)不可为 null。
六、修改课程实体
在ModelsCourse.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 virtual Department Department { get; set; }
public virtual ICollection<Enrollment> Enrollments { get; set; }
public virtual ICollection<Instructor> Instructors { get; set; }
}
}
课程实体具有外键属性,DepartmentID
指向相关的 Department
实体并且具有 Department
导航属性。 如果拥有相关实体的导航属性,则 Entity Framework 不会要求为数据模型添加外键属性。 EF 在需要时在数据库中自动创建外键。 但如果数据模型包含外键,则更新会变得更简单、更高效。 例如,当您提取课程实体进行编辑时,如果不加载它,则 Department
实体为 null,因此,在更新课程实体时,必须先提取 Department
实体。 如果数据模型中包含外键属性 DepartmentID
,则在更新前无需提取 Department
实体。
1、DatabaseGenerated 特性
CourseID
属性上带有None参数的DatabaseGenerated 属性指定,主键值由用户提供,而不是由数据库生成。
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
public int CourseID { get; set; }
默认情况下,实体框架假定数据库生成主键值。 大多数情况下,这是理想情况。 但是,对于 Course
实体,你将使用用户指定的课程编号,如一个部门的1000系列、另一个部门的2000系列等等。
2、外键和导航属性
Course
实体中的外键属性和导航属性反映以下关系:
向一个系分配课程后,出于上述原因,会出现
DepartmentID
外键和Department
导航属性。public int DepartmentID { get; set; } public virtual Department Department { get; set; }
参与一门课程的学生数量不定,因此
Enrollments
导航属性是一个集合:public virtual ICollection<Enrollment> Enrollments { get; set; }
一门课程可能由多位讲师讲授,因此
Instructors
导航属性是一个集合:public virtual ICollection<Instructor> Instructors { get; set; }
七、创建 Department 实体
用以下代码创建ModelsDepartment.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 virtual Instructor Administrator { get; set; }
public virtual ICollection<Course> Courses { get; set; }
}
}
1、列属性
之前,你已使用列属性更改列名映射。 在 Department
实体的代码中,Column
特性用于更改 SQL 数据类型映射,以便使用数据库中的 SQL Server money类型定义列:
[Column(TypeName="money")]
public decimal Budget { get; set; }
通常不需要列映射,因为实体框架通常基于您为属性定义的 CLR 类型选择适当的 SQL Server 数据类型。 CLR decimal
类型会映射到 SQL Server decimal
类型。 但在这种情况下,您知道列将包含货币金额,而money数据类型则更适合这样做。 有关 CLR 数据类型以及它们如何与 SQL Server 数据类型匹配的详细信息,请参阅SqlClient For Entity sqlclient 类型。
2、外键和导航属性
外键和导航属性可反映以下关系:
一个系可能有也可能没有管理员,而管理员始终是讲师。 因此,
InstructorID
属性包含为Instructor
实体的外键,在int
类型标识之后添加一个问号,以将该属性标记为可为 null。导航属性名为Administrator
,但包含Instructor
实体:public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }
一个部门可能有许多课程,因此存在一个
Courses
的导航属性:public virtual ICollection<Course> Courses { get; set; }
Note
按照约定,Entity Framework 能针对不可为 null 的外键和多对多关系启用级联删除。 这可能导致循环级联删除规则,尝试添加迁移时该规则会造成异常。 例如,如果未将
Department.InstructorID
属性定义为可为 null,则会收到以下异常消息: "引用关系将导致循环引用,这是不允许的。" 如果业务规则要求InstructorID
属性为不可为 null,则必须使用以下 Fluent API 语句禁用关系的级联删除:
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
八、修改 Enrollment 实体
在ModelsEnrollment.cs中,将之前添加的代码替换为以下代码
using System.ComponentModel.DataAnnotations;
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 virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
1、外键和导航属性
外键属性和导航属性可反映以下关系:
注册记录面向一门课程,因此存在
CourseID
外键属性和Course
导航属性:public int CourseID { get; set; } public virtual Course Course { get; set; }
注册记录面向一名学生,因此存在
StudentID
外键属性和Student
导航属性:public int StudentID { get; set; } public virtual Student Student { get; set; }
2、多对多关系
Student
和 Course
实体之间存在多对多关系,Enrollment
实体将作为具有数据库中的有效负载的多对多联接表。 这意味着,除了联接的表的外键(在本例中为主键和 Grade
属性)之外,Enrollment
表还包含其他数据。
下图显示这些关系在实体关系图中的外观。 (此关系图是使用实体框架 Power Tools生成的; 创建关系图不是本教程的一部分,它只是在此处用作说明。)
每个关系线一端有1个,另一个用星号(*)表示一对多关系。
如果 Enrollment
表未包括评分信息,只需包含两个外键 CourseID
和 StudentID
。 在这种情况下,它与数据库中没有负载(或纯联接表)的多对多联接表相对应,无需为其创建模型类。 Instructor
和 Course
实体具有这种类型的多对多关系,如您所见,它们之间没有实体类:
但是,数据库中需要联接表,如以下数据库关系图中所示:
实体框架会自动创建 CourseInstructor
表,并通过读取和更新 Instructor.Courses
并 Course.Instructors
导航属性,以间接方式读取和更新此表。
九、实体关系图
下图显示 Entity Framework Power Tools 针对已完成的学校模型创建的关系图。
除了多对多的关系线(* *)和一对多关系线(1到 *),可在此处查看 Instructor
和 OfficeAssignment
实体之间的一对零或一关系线(1到 0 ... 1)与指导员和部门实体之间的零或一对多关系线(0 .0 到 *)之间。
十、将代码添加到数据库上下文
接下来,将新实体添加到 SchoolContext
类,并使用Fluent API调用自定义某些映射。 API 是 "熟知的",因为它通常由排列一系列方法调用组合成单个语句,如以下示例中所示:
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
在本教程中,你将仅对不能使用属性执行的数据库映射使用 Fluent API。 但 Fluent API 还可用于指定大多数格式化、验证和映射规则,这可通过特性完成。 MinimumLength
等特性不能通过 Fluent API 应用。 如前文所述,MinimumLength
不会更改架构,它只应用客户端和服务器端验证规则。
某些开发者倾向于仅使用 Fluent API 以保持实体类的“纯净”。 如有需要,可混合使用特性和 Fluent API,且有些自定义只能通过 Fluent API 实现,但通常建议选择一种方法并尽可能坚持使用这一种。
若要将新实体添加到数据模型,并使用属性执行你不执行的数据库映射,请将DALSchoolContext.cs中的代码替换为以下代码:
using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace ContosoUniversity.DAL
{
public class SchoolContext : DbContext
{
public DbSet<Course> Courses { get; set; }
public DbSet<Department> Departments { get; set; }
public DbSet<Enrollment> Enrollments { get; set; }
public DbSet<Instructor> Instructors { get; set; }
public DbSet<Student> Students { get; set; }
public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
modelBuilder.Entity<Course>()
.HasMany(c => c.Instructors).WithMany(i => i.Courses)
.Map(t => t.MapLeftKey("CourseID")
.MapRightKey("InstructorID")
.ToTable("CourseInstructor"));
}
}
}
OnModelCreating方法中的新语句配置多对多联接表:
对于
Instructor
和Course
实体之间的多对多关系,代码指定联接表的表名和列名。 Code First 可以为你配置多对多关系,而无需此代码,但如果不调用此代码,则将获取InstructorID
列的默认名称,如InstructorInstructorID
。modelBuilder.Entity<Course>() .HasMany(c => c.Instructors).WithMany(i => i.Courses) .Map(t => t.MapLeftKey("CourseID") .MapRightKey("InstructorID") .ToTable("CourseInstructor"));
下面的代码提供了一个示例,说明如何使用 Fluent API 而不是属性来指定 Instructor
和 OfficeAssignment
实体之间的关系:
modelBuilder.Entity<Instructor>()
.HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);
有关 "Fluent API" 语句在幕后执行的操作的信息,请参阅 "流畅 API " 博客文章。
十一、使用测试数据设定数据库种子
将MigrationsConfiguration.cs文件中的代码替换为以下代码,以便为你创建的新实体提供种子数据。
namespace ContosoUniversity.Migrations
{
using ContosoUniversity.Models;
using ContosoUniversity.DAL;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.Migrations;
using System.Linq;
internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
{
public Configuration()
{
AutomaticMigrationsEnabled = false;
}
protected override void Seed(SchoolContext context)
{
var students = new List<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") }
};
students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var instructors = new List<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") }
};
instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
context.SaveChanges();
var departments = new List<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 }
};
departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
context.SaveChanges();
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 1045, Title = "Calculus", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 3141, Title = "Trigonometry", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2021, Title = "Composition", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = 2042, Title = "Literature", Credits = 4,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
var officeAssignments = new List<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" },
};
officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
context.SaveChanges();
AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
AddOrUpdateInstructor(context, "Chemistry", "Harui");
AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
AddOrUpdateInstructor(context, "Trigonometry", "Harui");
AddOrUpdateInstructor(context, "Composition", "Abercrombie");
AddOrUpdateInstructor(context, "Literature", "Abercrombie");
context.SaveChanges();
var enrollments = new List<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();
}
void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
{
var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
if (inst == null)
crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
}
}
}
正如你在第一个教程中看到的那样,此代码中的大多数只是更新或创建新的实体对象,并根据测试需要将示例数据加载到属性中。 不过,请注意如何处理与 Instructor
实体具有多对多关系的 Course
实体:
var courses = new List<Course>
{
new Course {CourseID = 1050, Title = "Chemistry", Credits = 3,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
...
};
courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
context.SaveChanges();
创建 Course
对象时,可以使用代码 Instructors = new List<Instructor>()
将 Instructors
导航属性初始化为空集合。 这样,便可以使用 Instructors.Add
方法添加与此 Course
相关的 Instructor
实体。 如果未创建空列表,则无法添加这些关系,因为 Instructors
属性为 null,并且没有 Add
方法。 你还可以将列表初始化添加到构造函数。
十二、添加迁移
在 PMC 中输入 add-migration
命令(尚不要执行 update-database
命令):
add-Migration ComplexDataModel
如果此时尝试运行 update-database
命令(先不要执行此操作),则会出现以下错误:
ALTER TABLE 语句与外键约束 "FK_dbo 冲突。当然_dbo。部门_DepartmentID "。在表 "dbo" 的数据库 "ContosoUniversity" 中发生冲突。部门 ",列" DepartmentID "。
有时,当您使用现有数据执行迁移时,您需要将存根(stub)数据插入到数据库中以满足外键约束,这就是您现在必须执行的操作。 ComplexDataModel Up
方法中生成的代码将不可以为 null 的 DepartmentID
外键添加到 Course
表中。 由于在代码运行时 Course
表中已有行,因此 AddColumn
操作将失败,因为 SQL Server 不知道要放入列中的值不能为 null。 因此,必须更改代码以为新列指定默认值,并创建一个名为 "Temp" 的存根部作为默认部门。 因此,在 Up
方法运行后,现有 Course
行将与 "Temp" 部门相关。 您可以将它们关联到 Seed
方法中的正确部门。
编辑 _ComplexDataModel.cs 文件>的 <时间戳,注释掉将 DepartmentID 列添加到课程表的代码行,然后添加以下突出显示的代码(已注释的行也将突出显示):
CreateTable(
"dbo.CourseInstructor",
c => new
{
CourseID = c.Int(nullable: false),
InstructorID = c.Int(nullable: false),
})
.PrimaryKey(t => new { t.CourseID, t.InstructorID })
.ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
.ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
.Index(t => t.CourseID)
.Index(t => t.InstructorID);
// Create a department for course to point to.
Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
// default value for FK points to department created above.
AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
//AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));
当 Seed
方法运行时,它将在 Department
表中插入行,并将现有 Course
行与新的 Department
行相关联。 如果你尚未在 UI 中添加任何课程,则不再需要 "Temp" 部门或 Course.DepartmentID
列上的默认值。 若要允许有人使用应用程序添加了课程,你还需要更新 Seed
方法代码,以确保所有 Course
行(而不仅仅是先前运行的 Seed
方法所插入的行)在从列中删除默认值并删除 "Temp" 部门之前都具有有效的 DepartmentID
值。
十三、更新数据库
编辑完 <时间戳>_ComplexDataModel.cs文件后,请在 PMC 中输入 update-database
命令以执行迁移。
update-database
Note
迁移数据和进行架构更改时,可能会收到其他错误。 如果遇到无法解决的迁移错误,你可以更改连接字符串中的数据库名称,或删除数据库。 最简单的方法是在web.config 文件中重命名数据库。 下面的示例显示更改为 CU_测试的名称:
<add name="SchoolContext" connectionString="Data Source=(LocalDb)v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
providerName="System.Data.SqlClient" />
对于新数据库,没有要迁移的数据,并且 update-database
命令更有可能在没有错误的情况下完成。 有关如何删除数据库的说明,请参阅如何从 Visual Studio 2012 中删除数据库。
如果此操作失败,可以尝试的另一种方法是,通过在 PMC 中输入以下命令,重新初始化数据库:
update-database -TargetMigration:0
像之前一样,在服务器资源管理器中打开数据库,然后展开 "表" 节点以查看是否已创建所有表。 (如果您仍在之前打开服务器资源管理器,请单击 "刷新" 按钮。)
你没有为 CourseInstructor
表创建模型类。 如前所述,这是 Instructor
与 Course
实体之间的多对多关系的联接表。
右键单击 "CourseInstructor
" 表,并选择 "显示表数据",以验证它是否具有添加到 Course.Instructors
导航属性的 Instructor
实体的结果中的数据。
十四、获取代码
十五、其他资源
可在ASP.NET 数据访问-建议的资源中找到指向其他实体框架资源的链接。
十六、后续步骤
在本教程中,你将了解:
- 自定义数据模型
- 更新的学生实体
- 已创建 Instructor 实体
- 已创建 OfficeAssignment 实体
- 修改了课程实体
- 已创建部门实体
- 已修改注册实体
- 向数据库上下文添加了代码
- 已使用测试数据设定数据库种子
- 已添加迁移
- 已更新数据库
转到下一篇文章,了解如何读取和显示实体框架加载到导航属性中的相关数据。