Fork me on GitHub

MVC5 Entity Framework学习(6):创建复杂的数据模型

Contoso University示例程序演示了如何使用Entity Framework 6 Code First 和 Visual Studio 2013创建ASP.NET MVC 5应用程序。

在上一篇文章中你已经创建了由三个实体组成的简单的数据模型。在本文章中你将会添加更多的实体和关系,并且通过指定格式、验证和数据库映射规则来自定义数据模型。这里介绍两种自定义数据模型的方法:向实体类中添加属性和向数据库上下文类中添加代码。

下面是完成后的数据模型类图

20140904212753256

使用属性来自定义数据模型

在本节中你将学习如何通过使用指定的格式、验证和数据库映射规则属性来自定义数据模型,在接下来的章节中,你将通过向你已经创建的类或者为模型中剩余的实体类型创建的新类中添加属性来创建完整的School数据模型。

DataType属性

对于学生入学日期,所有的页面都是显示时间和日期,即使你只在意该字段中的日期部分。通过使用数据批注属性,你可以只添加一行代码就可以在每一个视图中使用特定的格式显示数据。要做到这一点,你需要向Student 类中的EnrollmentDate属性添加一个属性。

打开Models\Student.cs,添加System.ComponentModel.DataAnnotations命名空间,为EnrollmentDate属性添加DateType和DisplayFormat属性,如下所示

DataType属性指明了一个比数据库内部类型更加具体的数据类型,在这种情况下,我们要显示的仅仅是日期,而不是日期和时间。DataType Enumeration提供了多种数据类型,比如Date, Time, PhoneNumber, Currency, EmailAddress等。DataType属性同样可以让应用程序来自动提供特定类型,例如DataType.EmailAddress可以创建mailto:超链接,DataType.Date属性可以在支持HTML5的浏览器中创建一个日期选择器。DataType属性可以生成HTML5浏览器能够识别的HTML 5 data-(读数据破折号)属性,但DataType特性并不提供任何验证。

DataType.Date并没有指明日期的显示格式,默认情况下是根据服务器的CultureInfo来显示数据字段的格式。

DisplayFormat属性用来显示的指明要显示的日期格式

ApplyFormatInEditMode指明当该值在文本框中被编辑时也应该使用已指定的格式(但是对一些字段来说例如货币值,你可能不希望对文本框中的货币符号进行编辑)。

你可以只使用一个DisplayFormat属性,当通常比较好的做法是同时也使用DataType属性。DataType属性传达的是数据本身的语义而不是如何将它呈现在屏幕上,并且它提供了使用DisplayFormat时所不具备的优势:

  • 浏览器可以启用HTML5功能(比如显示日历控件,本地化的货币符号,电子邮件链接,客户端输入验证等)
  • 默认情况下,浏览器将使用基于本地区域设置的正确格式来呈现数据
  • DataType属性可以让MVC自动选择正确的字段模板来呈现数据(DisplayFormat使用字符串模板)

如果日期字段使用了DataType属性,你还必须指定DisplayFormat属性以确保在Chrome浏览器中能正确呈现该字段。

运行项目,打卡Students选项卡,可以注意到Enrollment Date列不再显示时间部分,同样在任何使用Student 模型的视图中都会如此。

20140904225241439

StringLength属性

你还可以使用属性来指定数据验证规则和验证错误信息,StringLength属性可以设定数据库中字段的最大长度并为ASP.NET MVC提供客户端和服务器端验证,当然你也可以使用该属性来设定字段的最小长度,但设定最小值并不影响数据库架构

假设对于名字字段你想要确保用户输入不能超过50个字符,你需要为LastName和FirstMidName属性添加StringLength属性来限制用户输入,如下所示

StringLength属性并不能防止用户输入空白字符,但你可以使用正则表达式来限制用户输入,例如下面的表达式要求第一个字符必须是大写,其余的字符是字母表中的字母。

MaxLength属性和StringLength功能相似,但不提供客户端验证。

运行项目并点击Students 选项卡,会出现如下错误:

The model backing the ‘SchoolContext’ context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269)

Entity Framework 检测到数据模型已经被更改并要求数据库架构也作出相应的更改,接下来将通过使用迁移功能在不丢失数据库中任何数据的情况下更新数据库架构。如果你修改了使用Seed方法生成的数据,那么在使用Seed方法中的AddOrUpdate方法时会将其更改回原始状态(AddOrUpdate相当于数据库中的”upsert”操作)。

在 Package Manager Console (PMC)中输入下列命令:

add-migration命令创建一个名为<timeStamp>_MaxLengthOnNames.cs的文件,该文件中有一个Up方法来更新数据库以匹配当前数据模型。update-database命令运行该方法。

Entity Framework在迁移文件名中使用时间戳以便按顺序执行迁移程序。在运行update-database命令之前,你可以创建多个迁移,所有的迁移会按照它们创建的顺序来执行。

运行项目,打开Create页面,在LastName文本框中输入超过50个字符,点击Create,客户端会验证此字段并显示错误消息:

20140906222317956

Column 属性

你还可以通过使用属性来控制如何将类和属性映射到数据库。假设你使用FirstMidName作为名称字段,因为该字段中还可能包含一个中间名。但是你希望将数据库列命名为FirstName,因为那些写数据库查询语句的用户已经习惯与使用该列名。要完成此映射,你需要使用Column 属性。

Column属性指定当数据库被创建时,Student表中与FirstMidName属性映射的列将被命名为FirstName。换句话说,当你在代码中使用Student.FirstMidName时,该值会从Student表中的FirstName列查询到。如果你没有指定列的名称,该列会使用属性名作为列名。

打开 Student.cs,添加 System.ComponentModel.DataAnnotations.Schema命名空间,并为FirstMidName添加Column属性,如下所示:

添加的Column属性会修改数据模型,所以它不再匹配数据库架构。在PMC中输入下列命令:

在Server Explorer中,双击Student表,打开Student表设计器:

20140906232136215

下面的截图中可以看到在没有应用前两次迁移时原来的列名,现在FirstMidName已经被命名为FirstName,这两列的数据最大长度都已经有MAX更改为50个字符

20140906232147213

你也可以使用Fluent API来实现数据库映射。

注意:如果你在完成所有实体类之前试图编译该应用程序,你会得到编译错误。

完成对Student实体的更改

20140906232600620

打开Models\Student.cs,使用下面的代码替换:

Required 属性

Required属性设置名称属性为必填字段,值类型的字段是不需要Required属性的,例如DateTime, int, double, 和float。值类型不能被赋值为null值,所以它们本身就被视为必填字段。你也可以使用带有最小长度参数的StringLengthsh属性来替换Required属性。

Display 属性

Display属性指定文本框的标题应该是”First Name”, “Last Name”, “Full Name”和”Enrollment Date”,而不是每一个实例中属性本身的名字(那些中间没有空格的单词)。

FullName计算属性

FullName是一个计算属性,它返回一个由其它两个属性相连接后的值,因此它只有get访问方法,数据库也不会生成对应的FullName列。

3.创建Instructor实体

20140907111928140

新建Models\Instructor.cs类,使用下面的代码替换:

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

你也可以将多个属性放在同一行上,如下所示:

Courses 和OfficeAssignment导航属性

Courses 和OfficeAssignment是导航属性,就像之前解释过的那样,它们通常被定义为virtual类型以便它们可以使用Entity Framework的延迟加载(lazy loading)功能。如果一个导航属性中包含有多个实体,则其类型必须实现ICollection<T>接口,例如List<T>而不是IEnumerable<T>,因为IEnumerable<T>并没有实现Add方法。

一个 instructor可以教多门course,所以Courses 被定义为Course实体的集合。

我们的业务规定一个instructor 最多只能有一个office,所以OfficeAssignment 被定义为单个OfficeAssignment 实体(如果instructor 没有office,则赋值为null)。

创建OfficeAssignment实体

20140907132219281

创建Models\OfficeAssignment.cs,使用下面的代码替换:

生成项目,确保不会出现任何错误

Key 属性

Instructor和OfficeAssignment实体之间是一对零或一对一的关系,office 的指派只和Instructor有关系,因此其主键也是其Instructor实体的外键。但是Entity Framework 不会自动将InstructorID识别为实体的主键,因为该名称并不遵守ID 或者classnameID的命名规范,因此这里使用Key属性来指定该属性为实体的主键。

如果实体不存在主键,但是你希望将属性命名为不同于classnameID 或 ID的名称,那么你可以使用Key属性。默认情况下,EF将Key作为非数据库生成的,因为该列用来标识关系。

ForeignKey属性

当两个实体之间是一对零或一对一关系时(如OfficeAssignment 和Instructor实体间的关系),EF并不能辨别出关系的哪一端是主体,哪一端是依赖。一对一的关系在每一个类中拥有一个对其他类的导航属性的引用。ForeignKey属性可以被应用于依赖类来建立它们之间的关系。如果你省略了ForeignKey属性,当你试图创建迁移时会出现如下错误:

Unable to determine the principal end of an association between the types ‘ContosoUniversity.Models.OfficeAssignment’ and ‘ContosoUniversity.Models.Instructor’. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

Instructor导航属性

Instructor实体有一个值为nullable 的OfficeAssignment导航属性(因为instructor 可能没有被分配office),OfficeAssignment实体有一个值为non-nullable的Instuctor导航属性(因为office不可能在没有instructor 的情况下被分配出去–InstructorID值为non-nullable)。当一个Instructor实体有一个相关联的OfficeAssignment实体时,每个实体在它的导航属性中都有对其它实体的引用。

你可以将Required属性添加到Instructor导航属性来指定必须有一个相关联的Instructor,但是这不是必需的,因为InstructorID外键(同样也是表的主键)是non-nullable的。

修改Course实体

20140907144004482

打开Models\Course.cs,使用下面的代码替换:

course 实体有一个名为DepartmentID的指向相关联的Department实体的外键属性,该实体还有一个Department导航属性。当一个相关联实体有一个导航属性时, Entity Framework并不需要你将外键属性添加到数据模型, Entity Framework会在需要的任何地方自动创建外键属性,但是数据模型中的外键属性会让更新更简单、更高效。例如,当你检索一个Course 实体并进行编辑时,如果你不不加载Department实体的话,该实体为null,所以当你更新Course 时,你必须先检索Department实体。当数据模型包含名为DepartmentID的外键属性时,你就不需要在更新前再次检索Department实体。

DatabaseGenerated属性

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

默认情况下,Entity Framework假定主键值是由数据库生成的,在大多数情况下都是如此,然而,对于Course 实体,你将会使用用户指定的Course编号比如1000系列表示一个department,2000系列表示另一个department等等。

外键和导航属性

Course 实体中的外键属性和导航属性反映了以下关系:

  • 一个course 被分配到一个department,所以该实体中存在一个DepartmentID 外键和一个Department 导航属性。
  • 一个course可以有任意数量的student选修,所以Enrollments导航属性是一个集合。
  • 一个course可以由多个instructor来讲授,所以Instructors 导航属性也是一个集合。

创建Department实体

20140907152013297

创建Models\Department.cs,使用下面的代码替换:

Column属性

之前你使用了Column属性来更改列名,在Department实体的代码中,Column属性被用来更改SQL数据类型映射以便使用SQL Server的money类型来定义该列。

列映射通常并不是必需的,因为Entity Framework通常会基于你为属性定义的CLR类型来选择适当的SQL Server 数据类型。CLR decimal类型与SQL Server decimal类型相映射,但在当前情况下,该列应该保存货币数额,所以money数据类型更合适该列。

外键和导航属性

外键和导航属性反映了如下关系:

  • 一个department可能有也可能没有administrator,一个administrator是一个instructor,因此InstructorID属性被作为Instructor实体的外键。在int类型后面添加了问号表示该属性是值可以为nullable。导航属性被命名为Administrator并含有一个Instructor实体。
  • 一个department 可以有多门 course,富所以其有一个Courses导航属性

注意:基于约定,Entity Framework对于 non-nullable外键和多对多关系会启用级联删除,级联删除规则可能会在你添加迁移时导致异常出现。例如,如果你没有将Department.InstructorID属性定义为nullable,你会得到如下异常信息:“The referential relationship will result in a cyclical reference that’s not allowed.”。如果你的业务规则需要InstructorID属性可为non-nullable,你必须使用下面的fluent API语句来禁用级联删除。

修改Enrollment实体

20140907225929950

打开Models\Enrollment.cs,使用下面的代码替换:

外键和导航属性

外键和导航属性反映了下列关系:

  • 一条enrollment 记录对应一门course,所以有CourseID外键属性和Course导航属性:
  • 一条enrollment 记录对应一个student,所以有StudentID外键属性和Student导航属性:

多对多关系

Student 和Course 实体之间有多对多的关系,并且Enrollment 实体作为一个多对多的数据库连接表。这意味着Enrollment表包含了除了连接表外键之外的额外的数据(在本例中是主键和Grade属性)。

下图是实体关系图(此图是由 Entity Framework Power Tools生成的)

20140907231318468

每个关系连接线的一端都有个1,另一端是星号,表明这是一个一对多的关系。

如果Enrollment表不包含grade 信息,它只需要有CourseID和StudentID两个外键。在这种情况下,它只对应于数据库中的一个多对多连接表,并且你不需要为它们创建模型类。Instructor和Course实体是多对多关系,但如你所见,它们之间并没有实体类:

20140907232339411

在数据库中,连接表是必需的

20140908132940918

Entity Framework会自动创建CourseInstructor表,并通过读取和更新Instructor.Course和Course.Instructor导航属性来间接地读取和更新它。

在实体关系图中显示关系

下图显示了由Entity Framework Power Tools创建的完整的School 模型:

20140908133148515

除了多对多关系连接线(*到*)和一对多关系连接线(1到*),你还可以看到Instructor和OfficeAssignment实体之间的一对零或1关系连接线(1到0..1)和Istructor和Department实体之间的零或一对多(0..1到*)关系连接线。

向数据库上下文中添加代码到来自定义数据模型

接下来你将向SchoolContext类中添加新实体并使用fluent API来自定义映射。该API经常被用于在一个语句中同时调用多个方法,如下所示:

在本文中,你将在不能使用属性的地方使用fluent API来进行数据库映射。但是你也可以如同使用属性那样使用fluent API来指定大部分的格式、验证和映射规则。某些属性比如MinimumLength并不能通过使用fluent API来实现,就像之前提到的那样,MinimumLength不会更改数据库架构,它仅仅用于客户端和服务器端验证。

某些开发人员喜欢只使用fluent API以便他们可以保持他们的实体类”干净”。如果你愿意,你也可以同时使用属性和fluent API,要注意某些自定义功能只能通过使用fluent API来实现,但一般建议是仅选择这两者之一并尽可能的坚持使用下去。

向数据模型中添加新的实体并执行数据库映射,打开DAL\SchoolContext.cs,使用下面的代码替换

在OnModelCreating方法中使用了新语句来配置多对多连接表:

  • 对于Instructor和Course实体,上面的代码为连接表指定了表名和列名。Code First可以在不使用这段代码的情况下配置多对多关系,但是如果你不使用它,连接表会使用默认名称比如InstructorID列会被命名为InstructorInstructorID。

下面的代码举例说明了如何使用fluent API而不是使用属性来指定Instructor和OfficeAssignment实体之间的关系:

向数据库中填充测试数据

打开Migrations\Configuration.cs,使用下面的代码替换

正如你看到的那样,大部分代码更新或创建了新的实体对象并加载示例数据进行测试。但是,请注意这里是如何处理Course实体的,该实体与Instructor实体是多对多的关系。

当创建Course对象时,你使用代码Instructors = new List<Instructor>()将Instructor导航属性初始化为了一个空的集合,这样可以使用Instructors.Add方法来添加与Course实体相关联的Instructor实体。如果你没有将Instructor导航属性初始化为一个空的集合,你将不能添加这些关系,因为Instructors属性值为null,并且不会有Add方法。当然你也可以在构造函数中进行初始化。

添加迁移和更新数据库

在PMC中输入add-migration命令(先不要运行update-database命令):

如果这是你尝试运行update-database命令,会出现如下错误:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint “FK_dbo.Course_dbo.Department_DepartmentID”. The conflict occurred in database “ContosoUniversity”, table “dbo.Department”, column ‘DepartmentID’.
有时当你在存在数据的情况下执行迁移时,你需要将存根数据插入到数据库以满足外键约束,这就是我们现在要做的。ComplexDataModel中的Up方法为Course表添加一个非空的DepartmentID外键。由于Course表中已存在数据行,SQL Server不知道该向非空列中插入何值,所以AddColumn操作会失败。因此你必须修改代码为新的列提供一个默认值,并创建一个名为”Temp”的存根department 作为默认department 。默认情况下,当运行Up方法时,Course表中已存在的数据行会被关联到”Temp” department ,你可以在Seed方法中将它们关联到正确的department 。

编辑<timestamp>_ComplexDataModel.cs文件,注释掉为Course表添加DepartmentID 列的行,并使用下面的代码替换:

当Seed方法运行时,它会向Department表中插入数据,并会将已存在的Course行关联到新插入的Department行。如果你还没有添加任何course,你将不再需要”Temp” department 或者Course.DepartmentID列的默认值。考虑到别人可能已经通过应用程序添加了course,你也希望可以通过修改Seed方法以确保在你删除列的默认值并删除”Temp” department之前所有的Course列都应该拥有一个有效的DepartmentID值。

编辑完成<timestamp>_ComplexDataModel.cs 文件后,在PMC中输入update-database命令

注意:在迁移数据和更改架构时可能出现一些错误。如果你不能解决这些错误,你可以修改连接字符串中数据库的名字或者直接删除数据库。最简单的方法就是重命名Web.config文件中数据库的名字。

在新的数据库中并没有数据需要迁移,所以update-database命令会成功执行。但是如果上述方法也出现了错误,你还可以通过在PMC中输入下面的命令来重新初始化数据库

在Server Explorer中打开数据库,展开Tables 节点查看所有已经创建的表。
20140908151626648

你并没有为CourseInstructor表创建模型类,就像之前解释的那样,它是Instructor 和Course 实体之间多对多关系的连接表。

右键点击CourseInstructor表,选择Show Table Data来验证数据,该数据是通过向Course.Instructors导航属性添加Instructor实体而产生的 。

20140908152136215

项目源码:https://github.com/johnsonz/MvcContosoUniversity

 

作者:Johnson
原创文章,版权所有,转载请保留原文链接。

发表评论

电子邮件地址不会被公开。 必填项已用*标注