为ASP.NET MVC应用程序创建更复杂的数据模型

这是微软官方教程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属性上,如下面的代码所示:

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<Enrollment> Enrollments { get; set; }
    }
}

DataType特性用于执行比数据库内部类型更加具体的数据类型。在本示例中,我们只想保持对日期的跟踪,而不是日期及时间。DataType枚举提供了多种数据类型,比如日期,时间,电话号码,电子邮件等。DataType特性同样可以让应用程序来自动基于数据类型的特殊功能。例如DataType.EmailAddress可以创建mailto:的超链接,DataType.Date特性可以在支持HTML5的浏览器中创建一个日期选择器。DataType特性可以生成HTML5浏览器支持的HTML5 数据特性。要注意DataType特性并不提供任何验证。

DataType.Date不指定日期的显示格式。默认情况下, 数据字段的显示基于服务器本身的 CultureInfo 的默认格式。

DisplayFormat特性用于显示指定的日期格式:

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

ApplyFormatInEditMode设置指定该值在文本框中进行编辑时也同样适用已指定的格式(某些情况下可能并不适用,比如针对一个货币值,您可能不希望在文本框中显示一个货币符号并对其编辑)。

您可以单独使用DisplayFormat特性,但通常一个好注意是同时使用DataType这两者。DataType特性所传达的是数据本身的表述而不是如何将它呈现在屏幕上。下面列出了一些您可以考虑不使用DisplayFormat的情况:

  • 目标浏览器可以启用HTML5功能(比如显示日历控件,区域化的货币符号,电邮链接,一些客户端输入验证等。)。

  • 默认情况下,浏览器将使用基于您的区域设置的正确格式老呈现数据。

  • DataType特性可以让MVC自动选择正确的模板来呈现数据(DisplayFormat使用字符串模板)。

如果您在日期字段上使用DataType特性,您也应当指定DisplayFormat特性以确保该字段在Chrome浏览器中正确呈现。详细信息请参见 StackOverflow thread 。

有关如何在MVC中处理其他数据类型,请参阅中 MVC 5 Introduction: Examining the Edit Methods and Edit View 的国际化部分。

再次运行学生索引页您会注意到页面上不再显示时间部分,所有使用学生模型的视图都会有类似的改变。

StringLength特性

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

假设您想要确保用户不能输入超过50个字符的名称,如果要添加该限制,将StringLength特性添加到LastName和FirstMidName属性,如下面的示例:

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<Enrollment> Enrollments { get; set; }
    }
}

StringLength特性不会阻止用户在姓名中输入空白字符,但您可以使用正则表达式属性来进行该限制。例如下面的代码要求第一个字符必须是大写,其余的字符是字母。

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

MaxLength特性提供的功能类似于StringLength,但不提供客户端验证。

运行程序并单击学生选项卡,您会收到以下的异常:

“System.InvalidOperationException”类型的异常在 EntityFramework.dll 中发生,但未在用户代码中进行处理 其他信息: 支持“SchoolContext”上下文的模型已在数据库创建后发生更改。请考虑使用 Code First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。

实体框架会检测到数据模型已经进行了更改并且要求数据库架构也作出相应的改变。您将通过使用迁移来在不丢失数据的情况下升级架构。如果您更改了使用Seed方法创建的数据,您在Seed方法中所使用的AddOrUpdate方法会更改回其原始状态(AddOrUpdate是一个相当于"upsert"操作的数据库术语)。

在程序包管理器控制台中,输入以下命令:

add-migration MaxLengthOnNames
update-database

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上,如下面的代码所示:

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<Enrollment> Enrollments { get; set; }
    }
}

Column特性改变了SchoolContext的模型,所以它不会匹配数据库。在程序包管理器通知台中创建另一个迁移,输入以下命令:

add-migration ColumnFirstName
update-database

在服务器资源管理器中,双击Student表,打开表格设计器。

您可以看到FirstMidName已经被命名为FirstName,而两个列的数据最大长度都已经变更为50个字符。

您还可以使用 Fluent API 来更改数据库映射,后面的教程我们将演示该做法。

注意:如果您在全部完成以下各节中创建的所有实体类之前尝试编译程序,您会收到编译器错误。

完成对学生实体的更改

在Model\Student.cs中,使用下面的代码替换原来的内容:

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 FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

必需特性

Required特性使属性成为必需的字段。值类型的字段是不需要Required特性的,比如Int,double,DateTime等,由于值类型不能分配Null值,所以它们本身就被视为必需字段。您也可以删除Required特性并使用带有最小长度的StringLength特性来替换它。

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

显示特性

Display特性指定文本框中的标题应该是"姓","名","全名","注册日期"而不是属性本身的名字。

FullName计算属性

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

创建讲师实体

创建Models\Instructor.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 = "姓")]
        [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<Course> Courses { get; set; }
        public virtual OfficeAssignment OfficeAssignment { get; set; }
    }
}

请注意讲师实体和学生实体的属性都是相同的,在后续的教程中,您会通过执行继承来重构此代码以消除冗余。

您也可以将多个特性放在一行上,如下面所示:

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> Courses { get; set; }
   public virtual OfficeAssignment OfficeAssignment { get; set; }
}

Course和OfficeAssignment导航属性

Course和OfficeAssignment是导航属性。正如之前所解释的,它们通常被定义为virtual,这样它们就可以利用实体框架中的延迟加载。此外,如果一个导航属性可以容纳多个实体,则它的类型必须实现ICollection<T>接口,例如List<T>,但不能是IEnumerable<T>,因为它不实现Add。

教师可以教授任意数量的课程,所以Courses定义为Course实体的集合。

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

我们的业务逻辑定义一个讲师只能有一个办公室,因此OfficeAssignment定义为单个OfficeAssignment的实体(如果讲师没有办公室,则可以分配Null)。

        public virtual OfficeAssignment OfficeAssignment { get; set; }

创建OfficeAssignment实体

创建Models\OfficeAssignment.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 = "办公室地址")]
        public string Location { get; set; }

        public virtual Instructor Instructor { get; set; }
    }
}

全部保存后生成项目,确保没有弹出任何编译器或可以捕捉的错误。

Key特性

Instructor和OfficeAssignment实体之间有一个对零或一对一的关系。办公室只和讲师之间存在关系,因此其主键也是其Instructor实体的外键。但是实体框架不会自动将InstructorID识别为实体的主键,因为该命名不遵循实体框架约定。因此,我们使用Key特性来标记该属性为实体的主键:

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

如果实体没有它自己的主键,但您想将属性名命名为类名-ID或ID以外的不同的名称,您同样可以使用Key特性。默认情况下实体框架将键视为非数据库生成的,因为该列用来标识关系。

ForeignKey特性

当两个实体之间存在有一对零或一对一关系时,实体框架无法自动辨认出那一端的关系是主体,那一端是依赖。一对一关系在每个类中使用导航属性来引用其他类。ForeignKey特性可以应用于要建立关系的依赖类。如果您省略ForeignKey特性,当您尝试创建迁移时系统会出现一个无法确定实体间关系的错误。

Instructor导航属性

Instructor实体有一个可为空的OfficeAssignment导航属性(因为可能有讲师没有分配办公室),并且OfficeAssignment实体有一个不可为空的Instuctor导航属性(因为一个办公室不可能在没有讲师的情况下分配出去--InstructorID是不可为空的)。当Instructor实体具有OfficeAssignment实体关联的时候,每个实体在导航属性中都有另一个的引用。

您可以把一个Required特性添加给Instructor导航属性来指明必须有相关的讲师,但您不需要这样做。因为InstructorId外键(同样也是表的主键)是不可为null的。

修改Course实体

在Models\Course.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 = "编号")]
        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导航属性。当一个关联实体有一个导航属性时,实体框架不需要您添加外键属性到您的实体模型,实体框架在需要时会在数据库中自动创建外键。但在实体模型中拥有一个外键会让更新更简单、高效。例如,当您读取一个Course实体进行编辑,如果您选择不加载Department实体,那Department实体是空的。所以当您更新Course实体时,您必须先取得该实体关联的Department实体。如果在数据模型中包含了外键DepartmentID,您就不需要在更新前先取得Department实体。

DatabaseGenerated特性

CourseID属性有一个提供了None参数的DatabaseGenerated特性,该特性指明主键值将由用户提供,而不是由数据库自动生成。

        [DatabaseGenerated(DatabaseGeneratedOption.None)]
        [Display(Name = "名称")]
        public int CourseID { get; set; }

默认情况下,实体框架会假定主键应当由数据库生成。这在大多数情况下都是必要的。然而对于Course实体,您会使用一个用户指定的课程编号,比如1000系列表示一类课程,2000系列是另一类等等。

外键和导航属性

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

创建系实体

使用下面的代码创建Models\Department.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 = "起始日期")]
        public DateTime StartDate { get; set; }

        public int? InstructorID { get; set; }

        public virtual Instructor Administrator { get; set; }
        public virtual ICollection<Course> Courses { get; set; }
    }
}

Column特性

之前您已经使用过Column特性来更改列名称映射。在系实体代码中,Column特性被用于更改SQL数据类型的映射,以指明列定义将在数据库中使用money类型。

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

列映射通常是不必要的,因为实体框架通常会基于您定义属性的CLR类型来自动选择适当的SQL SERVER数据类型作为数据列的类型。CLR的decimal类型是映射到SQL Server decimal类型的,但在这种情况下,您知道该属性将保存货币金额,所以指明了比decimal更适合的money数据类型来作为列的数据类型。有关CLR数据类型及它们如何匹配到SQL Server数据类型的详细信息,请参见 SqlClient for Entity FrameworkTypes 。

外键和导航属性

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

  • 一个系可能没有管理员,但管理员始终是一名讲师。因此,InstructorID属性是Instructor实体的外键,并且在属性类型后添加了一个问号,将其标记为可空的。导航属性被命名为Administrator,但持有Instructor实体。 public int? InstructorID { get; set; } public virtual Instructor Administrator { get; set; }

  • 一个系可能有很多课程,所以有一个Courses导航属性 public virtual ICollection<Course> Courses { get; set; }

注意:基于约定,实体框架针对非空外键和多对多关系会启用级联删除。这可能会导致循环的级联删除规则,使得在您尝试添加一个迁移时导致一场。例如,如果您不将Department.InstructorID属性定义为可为空的,您会得到一个引用关系错误的异常。如果您的业务规则需要InstructorID属性设置为不可为空,则必须使用以下fluent API来声明在关系上禁用级联删除。

modelBuilder.Entity().HasRequired(d => d.Administrator).WithMany().WillCascadeOnDelete(false);

修改Enrollment实体

在Models\Enrollment.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 = "没有成绩")]
        public Grade? Grade { get; set; }

        public virtual Course Course { get; set; }
        public virtual Student Student { get; set; }
    }
}

外键和导航属性

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

  • 一条注册记录对应单个课程,因此,有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; }

多对多关系

在学生和课程之间有多对多的关系,并且注册实体作为一个多对多的数据库连接表。这意味着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经常使用一系列的方法调用来合并为单个语句,如下面的示例:

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

在本教程中,您将在不使用特性来进行的数据库映射的部分使用fluent API,但您还可以如同使用大多数特性那样来使用fluent API指定格式、验证和映射规则。某些特性不能使用fluent API,例如MinimumLength,如前文所述,MinimumLength不会更改数据库架构,它仅适合用户客户端和服务器端验证。

某些开发人员喜欢完全使用fluent API来保持它们的实体类"干净"。如果您想要的话,您可以混用特性和fluent API,要注意某些自定义功能呢只能使用fluent API来实现,但一般建议是仅选择这两者中之一。

要添加新的实体模型数据到数据模型并且执行没有使用特性的数据库映射,请将DAL\SchoolContext.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的更多信息,请参阅 Fluent API 。

填充测试数据到数据库中

将Migrations\Configuration.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));
        }
    }
}

正如您之前在第一个教程中看到的,大部分代码只是简单地更新或创建了新的实体对象并且读取测试数据到属性用于测试。但是请注意Course实体,它和Instructor实体之间存在多对多的关联:

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对象时,Instructor导航属性被使用代码Instructors = new List<Instructor>()来初始化为一个空的集合。这使它能够使用Instructors.Add方法来添加Instructor实体相关的Course实体。如果您没有创建一个空的列表,您就不能够进行添加,因为Instructors属性为null,所以也不会有一个Add方法来添加这些关系。您同样可以在构造函数中初始化该列表。

添加迁移和更新数据库

从程序包管理器控制台中,输入add-migration命令(先不要运行update-database命令):

add-Migration ComplexDataModel

如果您试图再次运行update-database命令,您会收到一个外键冲突错误。

当您在保存现有数据的状态下执行迁移时,您需要将存根数据插入到数据库以满足外键约束要求,所以我们现在就来做这些。在ComplexDataModel中的up方法生成的代码将为Course数据表添加一个非空DepartmentID外键。因为针对Course数据表中的已有行执行代码时AddColumn操作将失败,因为SQL Server不知道使用何值来填充不可为空的列。因此,必须更改代码,提供一个默认值给新列,并创建一个"Temp"作为默认系的存根。因此,在Up方法中使用Temp系来分配给Course的现有行。您可以在Seed方法中重新分配给它们正确的系。

编辑<时间戳>_ComplexDataModel.cs文件,注释掉Course数据表中添加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方法代码来确保在您从列中删除默认值和Temp系之前所有Course行(不光是较早由Seed方法插入的)都具有有效的DepartmentID值。

编辑完文件后,在程序包资源管理器中输入update-database命令。

updata-database

注意,在迁移数据并进行架构变更时您可能会得到某些错误,如果您不能解决迁移错误,您可以尝试更改连接字符串的名称或删除数据库。最简单的方法是重命名web.config文件中的数据库名称来创建一个新的。

新的数据库没有数据需要迁移,并且updata-database命令更有可能在没有错误的情况下完成。如果失败,您可以尝试重新初始化数据库,通过输入以下命令:

update-database -TargetMigration:0

在服务器资源管理器中打开数据库,展开表节点来观察是否所有的表都已经成功创建(如果您较早已经打开过,尝试刷新一下)。

您并没有针对CourseInstructor数据表创建数据模型,如前所述,这是Instructor和Course实体之间的多对多关系的连接表。

右键单击CourseInstructor表,选择显示表数据以验证其中的数据。

总结

您现在拥有一个更加复杂的数据模型和显影的数据库。在之后的教程中您会了解更多关于使用不同方式来访问数据的方法。

作者信息

Tom Dykstra - Tom Dykstra 是微软Web平台及工具团队的高级程序员,作家。