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

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

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

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

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

DataType属性

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

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

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

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属性用来显示的指明要显示的日期格式

  1. [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

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

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

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

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

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

StringLength属性

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

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

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

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

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

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)中输入下列命令:

  1. add-migration MaxLengthOnNames
  2. update-database

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

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

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

Column 属性

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

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

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel.DataAnnotations;
  4. using System.ComponentModel.DataAnnotations.Schema;
  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. [Column("FirstName")]
  14. public string FirstMidName { get; set; }
  15. [DataType(DataType.Date)]
  16. [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
  17. public DateTime EnrollmentDate { get; set; }
  18. public virtual ICollection<Enrollment> Enrollments { get; set; }
  19. }
  20. }

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

  1. add-migration ColumnFirstName
  2. update-database

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


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

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

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

2.完成对Student实体的更改

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

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

Required 属性

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

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

Display 属性

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

FullName计算属性

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

3.创建Instructor实体

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

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

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

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

  1. public class Instructor
  2. {
  3. public int ID { get; set; }
  4. [Display(Name = "Last Name"),StringLength(50, MinimumLength=1)]
  5. public string LastName { get; set; }
  6. [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
  7. public string FirstMidName { get; set; }
  8. [DataType(DataType.Date),Display(Name = "Hire Date")]
  9. public DateTime HireDate { get; set; }
  10. [Display(Name = "Full Name")]
  11. public string FullName
  12. {
  13. get { return LastName + ", " + FirstMidName; }
  14. }
  15. public virtual ICollection<Course> Courses { get; set; }
  16. public virtual OfficeAssignment OfficeAssignment { get; set; }
  17. }

Courses 和OfficeAssignment导航属性

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

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

  1. public virtual ICollection<Course> Courses { get; set; }

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

  1. public virtual OfficeAssignment OfficeAssignment { get; set; }

4.创建OfficeAssignment实体

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

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

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

Key 属性

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

  1. [Key]
  2. [ForeignKey("Instructor")]
  3. public int InstructorID { get; set; }

如果实体不存在主键,但是你希望将属性命名为不同于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的。

5.修改Course实体

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

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

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

DatabaseGenerated属性

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

  1. [DatabaseGenerated(DatabaseGeneratedOption.None)]
  2. [Display(Name = "Number")]
  3. public int CourseID { get; set; }

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

外键和导航属性

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

  • 一个course 被分配到一个department,所以该实体中存在一个DepartmentID 外键和一个Department 导航属性。

    1. public int DepartmentID { get; set; }
    2. public virtual Department Department { get; set; }
  • 一个course可以有任意数量的student选修,所以Enrollments导航属性是一个集合。
    1. public virtual ICollection<Enrollment> Enrollments { get; set; }
  • 一个course可以由多个instructor来讲授,所以Instructors 导航属性也是一个集合。
    1. public virtual ICollection<Instructor> Instructors { get; set; }

6.创建Department实体

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

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

Column属性

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

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

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

外键和导航属性

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

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

    1. public int? InstructorID { get; set; }
    2. public virtual Instructor Administrator { get; set; }
  • 一个department 可以有多门 course,富所以其有一个Courses导航属性
    1. public virtual ICollection<Course> Courses { get; set; }

注意:基于约定,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语句来禁用级联删除。

7.修改Enrollment实体

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

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

外键和导航属性

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

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

    1. public int CourseID { get; set; }
    2. public virtual Course Course { get; set; }
  • 一条enrollment 记录对应一个student,所以有StudentID外键属性和Student导航属性:
    1. public int StudentID { get; set; }
    2. public virtual Student Student { get; set; }

多对多关系

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

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

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

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

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

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

8.在实体关系图中显示关系

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

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

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

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

  1. modelBuilder.Entity<Course>()
  2. .HasMany(c => c.Instructors).WithMany(i => i.Courses)
  3. .Map(t => t.MapLeftKey("CourseID")
  4. .MapRightKey("InstructorID")
  5. .ToTable("CourseInstructor"));

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

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

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

  1. using ContosoUniversity.Models;
  2. using System.Data.Entity;
  3. using System.Data.Entity.ModelConfiguration.Conventions;
  4. namespace ContosoUniversity.DAL
  5. {
  6. public class SchoolContext : DbContext
  7. {
  8. public DbSet<Course> Courses { get; set; }
  9. public DbSet<Department> Departments { get; set; }
  10. public DbSet<Enrollment> Enrollments { get; set; }
  11. public DbSet<Instructor> Instructors { get; set; }
  12. public DbSet<Student> Students { get; set; }
  13. public DbSet<OfficeAssignment> OfficeAssignments { get; set; }
  14. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  15. {
  16. modelBuilder.Conventions.Remove<PluralizingTableNameConvention>();
  17. modelBuilder.Entity<Course>()
  18. .HasMany(c => c.Instructors).WithMany(i => i.Courses)
  19. .Map(t => t.MapLeftKey("CourseID")
  20. .MapRightKey("InstructorID")
  21. .ToTable("CourseInstructor"));
  22. }
  23. }
  24. }

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

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

    1. modelBuilder.Entity<Course>()
    2. .HasMany(c => c.Instructors).WithMany(i => i.Courses)
    3. .Map(t => t.MapLeftKey("CourseID")
    4. .MapRightKey("InstructorID")
    5. .ToTable("CourseInstructor"));

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

  1. modelBuilder.Entity<Instructor>()
  2. .HasOptional(p => p.OfficeAssignment).WithRequired(p => p.Instructor);

10.向数据库中填充测试数据

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

  1. namespace ContosoUniversity.Migrations
  2. {
  3. using ContosoUniversity.Models;
  4. using ContosoUniversity.DAL;
  5. using System;
  6. using System.Collections.Generic;
  7. using System.Data.Entity;
  8. using System.Data.Entity.Migrations;
  9. using System.Linq;
  10. internal sealed class Configuration : DbMigrationsConfiguration<SchoolContext>
  11. {
  12. public Configuration()
  13. {
  14. AutomaticMigrationsEnabled = false;
  15. }
  16. protected override void Seed(SchoolContext context)
  17. {
  18. var students = new List<Student>
  19. {
  20. new Student { FirstMidName = "Carson",   LastName = "Alexander",
  21. EnrollmentDate = DateTime.Parse("2010-09-01") },
  22. new Student { FirstMidName = "Meredith", LastName = "Alonso",
  23. EnrollmentDate = DateTime.Parse("2012-09-01") },
  24. new Student { FirstMidName = "Arturo",   LastName = "Anand",
  25. EnrollmentDate = DateTime.Parse("2013-09-01") },
  26. new Student { FirstMidName = "Gytis",    LastName = "Barzdukas",
  27. EnrollmentDate = DateTime.Parse("2012-09-01") },
  28. new Student { FirstMidName = "Yan",      LastName = "Li",
  29. EnrollmentDate = DateTime.Parse("2012-09-01") },
  30. new Student { FirstMidName = "Peggy",    LastName = "Justice",
  31. EnrollmentDate = DateTime.Parse("2011-09-01") },
  32. new Student { FirstMidName = "Laura",    LastName = "Norman",
  33. EnrollmentDate = DateTime.Parse("2013-09-01") },
  34. new Student { FirstMidName = "Nino",     LastName = "Olivetto",
  35. EnrollmentDate = DateTime.Parse("2005-09-01") }
  36. };
  37. students.ForEach(s => context.Students.AddOrUpdate(p => p.LastName, s));
  38. context.SaveChanges();
  39. var instructors = new List<Instructor>
  40. {
  41. new Instructor { FirstMidName = "Kim",     LastName = "Abercrombie",
  42. HireDate = DateTime.Parse("1995-03-11") },
  43. new Instructor { FirstMidName = "Fadi",    LastName = "Fakhouri",
  44. HireDate = DateTime.Parse("2002-07-06") },
  45. new Instructor { FirstMidName = "Roger",   LastName = "Harui",
  46. HireDate = DateTime.Parse("1998-07-01") },
  47. new Instructor { FirstMidName = "Candace", LastName = "Kapoor",
  48. HireDate = DateTime.Parse("2001-01-15") },
  49. new Instructor { FirstMidName = "Roger",   LastName = "Zheng",
  50. HireDate = DateTime.Parse("2004-02-12") }
  51. };
  52. instructors.ForEach(s => context.Instructors.AddOrUpdate(p => p.LastName, s));
  53. context.SaveChanges();
  54. var departments = new List<Department>
  55. {
  56. new Department { Name = "English",     Budget = 350000,
  57. StartDate = DateTime.Parse("2007-09-01"),
  58. InstructorID  = instructors.Single( i => i.LastName == "Abercrombie").ID },
  59. new Department { Name = "Mathematics", Budget = 100000,
  60. StartDate = DateTime.Parse("2007-09-01"),
  61. InstructorID  = instructors.Single( i => i.LastName == "Fakhouri").ID },
  62. new Department { Name = "Engineering", Budget = 350000,
  63. StartDate = DateTime.Parse("2007-09-01"),
  64. InstructorID  = instructors.Single( i => i.LastName == "Harui").ID },
  65. new Department { Name = "Economics",   Budget = 100000,
  66. StartDate = DateTime.Parse("2007-09-01"),
  67. InstructorID  = instructors.Single( i => i.LastName == "Kapoor").ID }
  68. };
  69. departments.ForEach(s => context.Departments.AddOrUpdate(p => p.Name, s));
  70. context.SaveChanges();
  71. var courses = new List<Course>
  72. {
  73. new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
  74. DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
  75. Instructors = new List<Instructor>()
  76. },
  77. new Course {CourseID = 4022, Title = "Microeconomics", Credits = 3,
  78. DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
  79. Instructors = new List<Instructor>()
  80. },
  81. new Course {CourseID = 4041, Title = "Macroeconomics", Credits = 3,
  82. DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
  83. Instructors = new List<Instructor>()
  84. },
  85. new Course {CourseID = 1045, Title = "Calculus",       Credits = 4,
  86. DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
  87. Instructors = new List<Instructor>()
  88. },
  89. new Course {CourseID = 3141, Title = "Trigonometry",   Credits = 4,
  90. DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
  91. Instructors = new List<Instructor>()
  92. },
  93. new Course {CourseID = 2021, Title = "Composition",    Credits = 3,
  94. DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
  95. Instructors = new List<Instructor>()
  96. },
  97. new Course {CourseID = 2042, Title = "Literature",     Credits = 4,
  98. DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
  99. Instructors = new List<Instructor>()
  100. },
  101. };
  102. courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
  103. context.SaveChanges();
  104. var officeAssignments = new List<OfficeAssignment>
  105. {
  106. new OfficeAssignment {
  107. InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID,
  108. Location = "Smith 17" },
  109. new OfficeAssignment {
  110. InstructorID = instructors.Single( i => i.LastName == "Harui").ID,
  111. Location = "Gowan 27" },
  112. new OfficeAssignment {
  113. InstructorID = instructors.Single( i => i.LastName == "Kapoor").ID,
  114. Location = "Thompson 304" },
  115. };
  116. officeAssignments.ForEach(s => context.OfficeAssignments.AddOrUpdate(p => p.InstructorID, s));
  117. context.SaveChanges();
  118. AddOrUpdateInstructor(context, "Chemistry", "Kapoor");
  119. AddOrUpdateInstructor(context, "Chemistry", "Harui");
  120. AddOrUpdateInstructor(context, "Microeconomics", "Zheng");
  121. AddOrUpdateInstructor(context, "Macroeconomics", "Zheng");
  122. AddOrUpdateInstructor(context, "Calculus", "Fakhouri");
  123. AddOrUpdateInstructor(context, "Trigonometry", "Harui");
  124. AddOrUpdateInstructor(context, "Composition", "Abercrombie");
  125. AddOrUpdateInstructor(context, "Literature", "Abercrombie");
  126. context.SaveChanges();
  127. var enrollments = new List<Enrollment>
  128. {
  129. new Enrollment {
  130. StudentID = students.Single(s => s.LastName == "Alexander").ID,
  131. CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID,
  132. Grade = Grade.A
  133. },
  134. new Enrollment {
  135. StudentID = students.Single(s => s.LastName == "Alexander").ID,
  136. CourseID = courses.Single(c => c.Title == "Microeconomics" ).CourseID,
  137. Grade = Grade.C
  138. },
  139. new Enrollment {
  140. StudentID = students.Single(s => s.LastName == "Alexander").ID,
  141. CourseID = courses.Single(c => c.Title == "Macroeconomics" ).CourseID,
  142. Grade = Grade.B
  143. },
  144. new Enrollment {
  145. StudentID = students.Single(s => s.LastName == "Alonso").ID,
  146. CourseID = courses.Single(c => c.Title == "Calculus" ).CourseID,
  147. Grade = Grade.B
  148. },
  149. new Enrollment {
  150. StudentID = students.Single(s => s.LastName == "Alonso").ID,
  151. CourseID = courses.Single(c => c.Title == "Trigonometry" ).CourseID,
  152. Grade = Grade.B
  153. },
  154. new Enrollment {
  155. StudentID = students.Single(s => s.LastName == "Alonso").ID,
  156. CourseID = courses.Single(c => c.Title == "Composition" ).CourseID,
  157. Grade = Grade.B
  158. },
  159. new Enrollment {
  160. StudentID = students.Single(s => s.LastName == "Anand").ID,
  161. CourseID = courses.Single(c => c.Title == "Chemistry" ).CourseID
  162. },
  163. new Enrollment {
  164. StudentID = students.Single(s => s.LastName == "Anand").ID,
  165. CourseID = courses.Single(c => c.Title == "Microeconomics").CourseID,
  166. Grade = Grade.B
  167. },
  168. new Enrollment {
  169. StudentID = students.Single(s => s.LastName == "Barzdukas").ID,
  170. CourseID = courses.Single(c => c.Title == "Chemistry").CourseID,
  171. Grade = Grade.B
  172. },
  173. new Enrollment {
  174. StudentID = students.Single(s => s.LastName == "Li").ID,
  175. CourseID = courses.Single(c => c.Title == "Composition").CourseID,
  176. Grade = Grade.B
  177. },
  178. new Enrollment {
  179. StudentID = students.Single(s => s.LastName == "Justice").ID,
  180. CourseID = courses.Single(c => c.Title == "Literature").CourseID,
  181. Grade = Grade.B
  182. }
  183. };
  184. foreach (Enrollment e in enrollments)
  185. {
  186. var enrollmentInDataBase = context.Enrollments.Where(
  187. s =>
  188. s.Student.ID == e.StudentID &&
  189. s.Course.CourseID == e.CourseID).SingleOrDefault();
  190. if (enrollmentInDataBase == null)
  191. {
  192. context.Enrollments.Add(e);
  193. }
  194. }
  195. context.SaveChanges();
  196. }
  197. void AddOrUpdateInstructor(SchoolContext context, string courseTitle, string instructorName)
  198. {
  199. var crs = context.Courses.SingleOrDefault(c => c.Title == courseTitle);
  200. var inst = crs.Instructors.SingleOrDefault(i => i.LastName == instructorName);
  201. if (inst == null)
  202. crs.Instructors.Add(context.Instructors.Single(i => i.LastName == instructorName));
  203. }
  204. }
  205. }

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

  1. var courses = new List<Course>
  2. {
  3. new Course {CourseID = 1050, Title = "Chemistry",      Credits = 3,
  4. DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
  5. Instructors = new List<Instructor>()
  6. },
  7. ...
  8. };
  9. courses.ForEach(s => context.Courses.AddOrUpdate(p => p.CourseID, s));
  10. context.SaveChanges();

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

11.添加迁移和更新数据库

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

  1. add-Migration ComplexDataModel

如果这是你尝试运行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 列的行,并使用下面的代码替换:

  1. CreateTable(
  2. "dbo.CourseInstructor",
  3. c => new
  4. {
  5. CourseID = c.Int(nullable: false),
  6. InstructorID = c.Int(nullable: false),
  7. })
  8. .PrimaryKey(t => new { t.CourseID, t.InstructorID })
  9. .ForeignKey("dbo.Course", t => t.CourseID, cascadeDelete: true)
  10. .ForeignKey("dbo.Instructor", t => t.InstructorID, cascadeDelete: true)
  11. .Index(t => t.CourseID)
  12. .Index(t => t.InstructorID);
  13. // Create  a department for course to point to.
  14. Sql("INSERT INTO dbo.Department (Name, Budget, StartDate) VALUES ('Temp', 0.00, GETDATE())");
  15. //  default value for FK points to department created above.
  16. AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false, defaultValue: 1));
  17. //AddColumn("dbo.Course", "DepartmentID", c => c.Int(nullable: false));
  18. AlterColumn("dbo.Course", "Title", c => c.String(maxLength: 50));

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

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

  1. update-database

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

  1. <add name="SchoolContext" connectionString="Data Source=(LocalDb)\v11.0;Initial Catalog=CU_Test;Integrated Security=SSPI;"
  2. providerName="System.Data.SqlClient" />

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

  1. update-database -TargetMigration:0

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

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

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

欢迎转载,请注明文章出处:http://blog.csdn.net/johnsonblog/article/details/39013469

MVC5 Entity Framework学习之创建复杂的数据模型的更多相关文章

  1. MVC5 Entity Framework学习

    MVC5 Entity Framework学习(1):创建Entity Framework数据模型 MVC5 Entity Framework学习(2):实现基本的CRUD功能 MVC5 Entity ...

  2. MVC5 Entity Framework学习之实现主要的CRUD功能

    在上一篇文章中,我们使用Entity Framework 和SQL Server LocalDB创建了一个MVC应用程序,并使用它来存储和显示数据.在这篇文章中,你将对由 MVC框架自己主动创建的CR ...

  3. MVC5 Entity Framework学习之Entity Framework高级功能(转)

    在之前的文章中,你已经学习了如何实现每个层次结构一个表继承.本节中你将学习使用Entity Framework Code First来开发ASP.NET web应用程序时可以利用的高级功能. 在本节中 ...

  4. MVC5 Entity Framework学习之Entity Framework高级功能

    在之前的文章中,你已经学习了怎样实现每一个层次结构一个表继承. 本节中你将学习使用Entity Framework Code First来开发ASP.NET web应用程序时能够利用的高级功能. 在本 ...

  5. MVC5 Entity Framework学习之实现继承

    之前你已经学习了怎样处理并发异常,在本节中你将学习怎样实现继承. 在面向对象的编程中,你能够使用继承来重用代码.接下来你将改动Instructor和Student类,让它们派生自Person基类,该基 ...

  6. MVC5 Entity Framework学习参加排序、筛选和排序功能

    上一篇文章实现Student 基本的实体CRUD操作.本文将展示如何Students Index页添加排序.筛选和分页功能. 以下是排序完成时.经过筛选和分页功能截图,您可以在列标题点击排序. 1.为 ...

  7. Entity Framework 学习整理(分播客整理)

    MSDN: http://msdn.microsoft.com/en-us/data/aa937723 台湾博客: http://www.dotblogs.com.tw/yc421206/ http: ...

  8. Entity Framework 学习整理

    MSDN: http://msdn.microsoft.com/en-us/data/aa937723 台湾博客: http://www.dotblogs.com.tw/yc421206/ http: ...

  9. Entity Framework 学习笔记(2)

    上期回顾:Entity Framework 学习笔记(1) Entity Framework最主要的东西,就是自己创建的.继承于DbContext的类: /// <summary> /// ...

随机推荐

  1. EnumMap

    以下内容基于jdk1.7.0_79源码: 什么是EnumMap Map接口的实现,其key-value映射中的key是Enum类型: 补充说明 其原理就是一个对象数组,数组的下标索引就是根据Map中的 ...

  2. php魔术方法罗列

    ##__sleep() 和 __wakeup() 当序列化(serialize)对象时,PHP 将试图在序列动作之前调用该对象的成员函数 __sleep() .__sleep() 方法常用于提交未提交 ...

  3. Web开发者宝典:10款流行前沿矢量图形素材

    矢量图形以其鲜亮.无杂斑和醒目的外观而深受网页设计师们的喜爱.本文整理了网页设计中最为流行的20款矢量设计素材,如网页按钮,社交媒体图标和联系人图标等,希望Web开发人员会喜欢. 1. Web But ...

  4. 小数量宽带用户的福音,Panabit 云计费easyradius 接口隆重发布,PA宽带计费系统

    PA接口在早前就发布了,但是一直迟迟没有发布官方说明文档,由于最近问的客户较多,特写了这篇文档 由于PA使用标准radius认证协议,所以用户需要在本地搭建一个计费,由于大部分用户的数量只有几百个,不 ...

  5. centos 下 yum安装和卸载软件

    安装的命令是,yum install xxx,yum会查询数据库,有无这一软件包,如果有,则检查其依赖冲突关系,如果没有依赖冲突,那么最好,下载安装;如果有,则会给出提示,询问是否要同时安装依赖,或删 ...

  6. Jquery 实现Xml文件内容处理

    用JS对XMl文件处理实现和用JS处理一般的Dom元素一样; 加载一个Xml内容与新建一个Dom元素基本相同 如: 1.新建一个Dom元素的Jquey语法为:$("<p>hell ...

  7. 柯南君:看大数据时代下的IT架构(6)消息队列之RabbitMQ--案例(Publish/Subscribe起航)

    二.Publish/Subscribe(发布/订阅)(using the Java Client) 为了说明这个模式,我们将构建一个简单的日志系统.它将包括两个项目: 第一个将发出日志消息 第二个将接 ...

  8. 根据输出设置select的被选中值

    $("#startupStatus").find("option").map(function(i) { if ($('#st-status').val() = ...

  9. IntelliJ IDEA 左侧列表设置忽略文件格式

    什么问题 idea 中设置忽略文件 Unity开发过程中使用Lua做逻辑开发 Unity会自动生成xx.meta文件 这种文件再使用Idea开发过程中没有用处 显示文件列表中会看着比较乱 如何设置 F ...

  10. 带着新人学springboot的应用06(springboot+RabbitMQ 中)

    上一节说了这么多废话,看也看烦了,现在我们就来用鼠标点点点,来简单玩一下这个RabbitMQ. 注意:这一节还是不用敲什么代码,因为上一节我们设置了那个可视化工具,我们先用用可视化工具熟悉一下流程. ...