[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序创建更复杂的数据模型
2020-12-13 02:00
标签:style blog class code java tar 这是微软官方教程Getting Started with Entity Framework 6 Code First using MVC 5
系列的翻译,这里是第六篇:为ASP.NET MVC应用程序创建更复杂的数据模型 原文:Creating a More Complex Data Model for an ASP.NET MVC
Application 译文版权所有,谢绝全文转载——但您可以在您的网站上添加到该教程的链接。 在之前的教程中您已经创建了由三个实体组成的简单的数据模型。在本教程中,您将会添加更多的实体和关系,您会进一步定制数据模型,包括指定格式、验证和数据库映射规则。您会看到两种自定义数据模型的方法:通过将属性添加到实体类和通过将代码添加到数据库上下文类。 当您完成时,实体类将组成一个完整的数据模型,如下图所示: 在本节中您会看到如何使用特性来定制数据模型的属性来用于指定格式、验证和数据库映射规则。在余下的章节中您将通过向已经创建的类中添加特性及创建剩余实体的新类来完善School数据模型。 对于学生注册日期,虽然您仅仅关心该字段中的日期,但在所有Web页面中都显示为日期和时间。通过使用数据批注特性,您可以使用代码来修复在每个视图中该字段的显示格式。为了实现这一点,您需要添加一个特性到学生类的EnrollmentDate属性。 在Models\Student.cs中,添加System.ComponentModel.DataAnnotations命名空间的using语句,将DateType及DisplayFormat特性添加到EnrollmentDate属性上,如下面的代码所示: DataType特性用于执行比数据库内部类型更加具体的数据类型。在本示例中,我们只想保持对日期的跟踪,而不是日期及时间。DataType枚举提供了多种数据类型,比如日期,时间,电话号码,电子邮件等。DataType特性同样可以让应用程序来自动基于数据类型的特殊功能。例如DataType.EmailAddress可以创建mailto:的超链接,DataType.Date特性可以在支持HTML5的浏览器中创建一个日期选择器。DataType特性可以生成HTML5浏览器支持的HTML5 数据特性。要注意DataType特性并不提供任何验证。 DataType.Date不指定日期的显示格式。默认情况下, 数据字段的显示基于服务器本身的CultureInfo的默认格式。 DisplayFormat特性用于显示指定的日期格式: ApplyFormatInEditMode设置指定该值在文本框中进行编辑时也同样适用已指定的格式(某些情况下可能并不适用,比如针对一个货币值,您可能不希望在文本框中显示一个货币符号并对其编辑)。 您可以单独使用DisplayFormat特性,但通常一个好注意是同时使用DataType这两者。DataType特性所传达的是数据本身的表述而不是如何将它呈现在屏幕上。下面列出了一些您可以考虑不使用DisplayFormat的情况: 如果您在日期字段上使用DataType特性,您也应当指定DisplayFormat特性以确保该字段在Chrome浏览器中正确呈现。详细信息请参见StackOverflow
thread。 有关如何在MVC中处理其他数据类型,请参阅中MVC
5 Introduction: Examining the Edit Methods and Edit View的国际化部分。 再次运行学生索引页您会注意到页面上不再显示时间部分,所有使用学生模型的视图都会有类似的改变。 您还可以使用特新来指定数据验证规则和验证错误信息。StringLength特性设定设定数据库的最大长度并且提供ASP.NET
MVC的客户端及服务器端验证。您还可以在此特性中指定字符串的最小长度,但最小值对数据库的架构没有任何影响。 假设您想要确保用户不能输入超过50个字符的名称,如果要添加该限制,将StringLength特性添加到LastName和FirstMidName属性,如下面的示例: StringLength特性不会阻止用户在姓名中输入空白字符,但您可以使用正则表达式属性来进行该限制。例如下面的代码要求第一个字符必须是大写,其余的字符是字母。 MaxLength特性提供的功能类似于StringLength,但不提供客户端验证。 运行程序并单击学生选项卡,您会收到以下的异常: “System.InvalidOperationException”类型的异常在 EntityFramework.dll
中发生,但未在用户代码中进行处理 其他信息: 支持“SchoolContext”上下文的模型已在数据库创建后发生更改。请考虑使用 Code
First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。 实体框架会检测到数据模型已经进行了更改并且要求数据库架构也作出相应的改变。您将通过使用迁移来在不丢失数据的情况下升级架构。如果您更改了使用Seed方法创建的数据,您在Seed方法中所使用的AddOrUpdate方法会更改回其原始状态(AddOrUpdate是一个相当于"upsert"操作的数据库术语)。 在程序包管理器控制台中,输入以下命令: add-migration命令创建一个名为_MaxLengthOnName.cs的文件,此文件包含用来更新数据库的Up方法,以匹配当前数据模型中的代码。update-database命令运行该代码。 实体框架使用有时间戳前缀的迁移文件名来进行迁移。您可以在运行update-database命令之前创建多个迁移,所有的迁移会按照它们创建的顺序来应用。 运行程序,新建一个学生,并在姓名中输入超过50个字符,点击创建,之后您会看到一条错误信息。 您还可以通过使用特性来控制如何将您的类和属性映射到数据库。假设您曾经使用名称FirstMidName来作为名称字段,因为该字段中还可能包含一个中间名。但您想让数据库中的列命名为FirstName,因为使用数据库来编写查询的其他用户都习惯于使用该列名。要做到这一点,您需要使用列特性。 Column特性指定在创建数据库时,Student表映射的FirstMidName属性的列将被命名为FirstName,换句话说,当您的代码引用Student.FirstMidName,相应的更新等改变会在数据库的Student表中的FirstName对应。如果您不指定列的名称,他们会使用和属性相同的名称。 在Student.cs文件中,添加 System.ComponentModel.DataAnnotations.Schema的引用,并将Column特性添加到FirstMidName上,如下面的代码所示: Column特性改变了SchoolContext的模型,所以它不会匹配数据库。在程序包管理器通知台中创建另一个迁移,输入以下命令: 在服务器资源管理器中,双击Student表,打开表格设计器。 您可以看到FirstMidName已经被命名为FirstName,而两个列的数据最大长度都已经变更为50个字符。 您还可以使用Fluent
API来更改数据库映射,后面的教程我们将演示该做法。 注意:如果您在全部完成以下各节中创建的所有实体类之前尝试编译程序,您会收到编译器错误。 在Model\Student.cs中,使用下面的代码替换原来的内容: Required特性使属性成为必需的字段。值类型的字段是不需要Required特性的,比如Int,double,DateTime等,由于值类型不能分配Null值,所以它们本身就被视为必需字段。您也可以删除Required特性并使用带有最小长度的StringLength特性来替换它。 Display特性指定文本框中的标题应该是"姓","名","全名","注册日期"而不是属性本身的名字。 FullName是一个计算的属性,通过串联其他两个属性来返回一个值。因此它只有get访问器,数据库也不会生成对应的FullName列。 创建Models\Instructor.cs,使用下面的代码替换默认生成的: 请注意讲师实体和学生实体的属性都是相同的,在后续的教程中,您会通过执行继承来重构此代码以消除冗余。 您也可以将多个特性放在一行上,如下面所示: Course和OfficeAssignment是导航属性。正如之前所解释的,它们通常被定义为virtual,这样它们就可以利用实体框架中的延迟加载。此外,如果一个导航属性可以容纳多个实体,则它的类型必须实现ICollection 教师可以教授任意数量的课程,所以Courses定义为Course实体的集合。 我们的业务逻辑定义一个讲师只能有一个办公室,因此OfficeAssignment定义为单个OfficeAssignment的实体(如果讲师没有办公室,则可以分配Null)。 创建Models\OfficeAssignment.cs并使用下面的代码替换自动生成的: 全部保存后生成项目,确保没有弹出任何编译器或可以捕捉的错误。 Instructor和OfficeAssignment实体之间有一个对零或一对一的关系。办公室只和讲师之间存在关系,因此其主键也是其Instructor实体的外键。但是实体框架不会自动将InstructorID识别为实体的主键,因为该命名不遵循实体框架约定。因此,我们使用Key特性来标记该属性为实体的主键: 如果实体没有它自己的主键,但您想将属性名命名为类名-ID或ID以外的不同的名称,您同样可以使用Key特性。默认情况下实体框架将键视为非数据库生成的,因为该列用来标识关系。 当两个实体之间存在有一对零或一对一关系时,实体框架无法自动辨认出那一端的关系是主体,那一端是依赖。一对一关系在每个类中使用导航属性来引用其他类。ForeignKey特性可以应用于要建立关系的依赖类。如果您省略ForeignKey特性,当您尝试创建迁移时系统会出现一个无法确定实体间关系的错误。 Instructor实体有一个可为空的OfficeAssignment导航属性(因为可能有讲师没有分配办公室),并且OfficeAssignment实体有一个不可为空的Instuctor导航属性(因为一个办公室不可能在没有讲师的情况下分配出去--InstructorID是不可为空的)。当Instructor实体具有OfficeAssignment实体关联的时候,每个实体在导航属性中都有另一个的引用。 您可以把一个Required特性添加给Instructor导航属性来指明必须有相关的讲师,但您不需要这样做。因为InstructorId外键(同样也是表的主键)是不可为null的。 在Models\Course.cs中,使用下面的代码替换自动原来的: 课程实体有一个DepartmentID外键属性,用来指向相关的Department实体,它有一个Department导航属性。当一个关联实体有一个导航属性时,实体框架不需要您添加外键属性到您的实体模型,实体框架在需要时会在数据库中自动创建外键。但在实体模型中拥有一个外键会让更新更简单、高效。例如,当您读取一个Course实体进行编辑,如果您选择不加载Department实体,那Department实体是空的。所以当您更新Course实体时,您必须先取得该实体关联的Department实体。如果在数据模型中包含了外键DepartmentID,您就不需要在更新前先取得Department实体。 CourseID属性有一个提供了None参数的DatabaseGenerated特性,该特性指明主键值将由用户提供,而不是由数据库自动生成。 默认情况下,实体框架会假定主键应当由数据库生成。这在大多数情况下都是必要的。然而对于Course实体,您会使用一个用户指定的课程编号,比如1000系列表示一类课程,2000系列是另一类等等。 Course实体中的外键和导航属性反映了以下关系: 使用下面的代码创建Models\Department.cs: 之前您已经使用过Column特性来更改列名称映射。在系实体代码中,Column特性被用于更改SQL数据类型的映射,以指明列定义将在数据库中使用money类型。 列映射通常是不必要的,因为实体框架通常会基于您定义属性的CLR类型来自动选择适当的SQL SERVER数据类型作为数据列的类型。CLR的decimal类型是映射到SQL
Server
decimal类型的,但在这种情况下,您知道该属性将保存货币金额,所以指明了比decimal更适合的money数据类型来作为列的数据类型。有关CLR数据类型及它们如何匹配到SQL Server数据类型的详细信息,请参见SqlClient for
Entity FrameworkTypes。 外键和导航属性反映了以下关系: 注意:基于约定,实体框架针对非空外键和多对多关系会启用级联删除。这可能会导致循环的级联删除规则,使得在您尝试添加一个迁移时导致一场。例如,如果您不将Department.InstructorID属性定义为可为空的,您会得到一个引用关系错误的异常。如果您的业务规则需要InstructorID属性设置为不可为空,则必须使用以下fluent
API来声明在关系上禁用级联删除。 在Models\Enrollment.cs中,使用下面的代码替换原来的: 外键和导航属性反映了下列关系: 在学生和课程之间有多对多的关系,并且注册实体作为一个多对多的数据库连接表。这意味着Enrollment数据表包含了连接表的外键除外的附加数据(在本例中,主键和Grade属性)。 下图显示了在实体关系图中这些关系的关联情况(本图使用实体框架Power Tools生成,创建关系图不是本教程的一部分,在此处仅仅是做示例)。 每个关系线都有一个结束和一个型号,表明这是一个一对多的关系。 如果Enrollment数据表不包含成绩信息,它只需要包含两个外键CourseID和StudentID,在这种情况下,他将会在数据库中对应无有效载荷(或纯连接表)的多对多连接表,您就无需针对它们单独创建一个模型类。Instructor和Course实体都有多对多关系,并且您可以看到,它们之间没有实体类: 数据库需要一个连接表,如下图的数据库关系图所示: 实体框架会自动创建CourseInstructor表,并通过Instructor.Course和Course.Instructor导航属性来间接地读取和更新它。 下面的插图显示了使用是实体框架Power Tools创建的完整的学校模型: 除了多对多关系线(*到*)和一对多关系线(1到*),您还能看到Instructor和OfficeAssignment实体之间的一到零或1关系线(1到0..1),以及Istructor和Department实体之间的零或一对多(0..1到*)关系线。 下一步您将添加新实体到SchoolContext类中并使用fluent API来自定义映射。该API经常使用一系列的方法调用来合并为单个语句,如下面的示例: 在本教程中,您将在不使用特性来进行的数据库映射的部分使用fluent API,但您还可以如同使用大多数特性那样来使用fluent
API指定格式、验证和映射规则。某些特性不能使用fluent
API,例如MinimumLength,如前文所述,MinimumLength不会更改数据库架构,它仅适合用户客户端和服务器端验证。 某些开发人员喜欢完全使用fluent API来保持它们的实体类"干净"。如果您想要的话,您可以混用特性和fluent
API,要注意某些自定义功能呢只能使用fluent API来实现,但一般建议是仅选择这两者中之一。 要添加新的实体模型数据到数据模型并且执行没有使用特性的数据库映射,请将DAL\SchoolContext.cs中的代码使用下面的替换: 在OnModelCreating方法中,我们使用了新语句来配置多对多连接表: 下面的代码举例说明如果使用fluent API而不是特性来指定Instructor和OfficeAssignment实体之间的关系: 有关fluent API的更多信息,请参阅Fluent
API。 将Migrations\Configuration.cs文件中的代码使用下面的替换:使用特性来定制数据模型
数据类型特性
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
StringLength特性
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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 = "名字不能超过50个字符")]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
public virtual ICollection
[RegularExpression(@"^[A-Z]+[a-zA-Z‘‘-‘\s]*$")]
add-migration MaxLengthOnNames
update-database
列特性
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
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 = "名字不能超过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 virtual ICollection
add-migration ColumnFirstName
update-database
完成对学生实体的更改
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Student
{
public int ID { get; set; }
[Required]
[Display(Name = "姓")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[StringLength(50, ErrorMessage = "名字不能超过50个字符")]
[Column("FirstName")]
[Display(Name = "名")]
public string FirstMidName { get; set; }
[Display(Name = "注册日期")]
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
public DateTime EnrollmentDate { get; set; }
[Display(Name = "全名")]
public string Name
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection
必需特性
[Display(Name = "Last Name")]
[StringLength(50, MinimumLength=1)]
public string LastName { get; set; }
显示特性
FullName计算属性
创建讲师实体
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 = "姓")]
[StringLength(50)]
public string LastName { get; set; }
[Required]
[Column("FirstName")]
[Display(Name = "名")]
[StringLength(50)]
public string FirstMidName { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}",ApplyFormatInEditMode = true)]
[Display(Name = "聘用日期")]
public DateTime HireDate { get; set; }
[Display(Name="全名")]
public string FullName
{
get
{
return LastName + ", " + FirstMidName;
}
}
public virtual ICollection
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")]
public DateTime HireDate { get; set; }
[Display(Name = "Full Name")]
public string FullName
{
get { return LastName + ", " + FirstMidName; }
}
public virtual ICollection
Course和OfficeAssignment导航属性
public virtual ICollection
public virtual OfficeAssignment OfficeAssignment { get; set; }
创建OfficeAssignment实体
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 = "办公室地址")]
public string Location { get; set; }
public virtual Instructor Instructor { get; set; }
}
}
Key特性
[Key]
[ForeignKey("Instructor")]
public int InstructorID { get; set; }
ForeignKey特性
Instructor导航属性
修改Course实体
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace ContosoUniversity.Models
{
public class Course
{
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "名称")]
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
DatabaseGenerated特性
[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "名称")]
public int CourseID { get; set; }
外键和导航属性
public int DepartmentID { get; set; }
public virtual Department Department { get; set; }
public virtual ICollection
public virtual ICollection
创建系实体
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 = "起始日期")]
public DateTime StartDate { get; set; }
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection
Column特性
[Column(TypeName = "money")]
public decimal Budget { get; set; }
外键和导航属性
public int? InstructorID { get; set; }
public virtual Instructor Administrator { get; set; }
public virtual ICollection
modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);
修改Enrollment实体
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 = "没有成绩")]
public Grade? Grade { get; set; }
public virtual Course Course { get; set; }
public virtual Student Student { get; set; }
}
}
外键和导航属性
public int CourseID { get; set; }
public virtual Course Course { get; set; }
public int StudentID { get; set; }
public virtual Student Student { get; set; }
多对多关系
在实体关系图中显示关系
添加代码到数据库上下文来自定义数据模型
modelBuilder.Entity
using ContosoUniversity.Models;
using System.Data.Entity;
using System.Data.Entity.ModelConfiguration.Conventions;
namespace ContosoUniversity.DAL
{
public class SchoolContext : DbContext
{
public DbSet
modelBuilder.Entity
modelBuilder.Entity
填充测试数据到数据库中
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
文章标题:[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为ASP.NET MVC应用程序创建更复杂的数据模型
文章链接:http://soscw.com/essay/24673.html