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

在前面的教程中,你使用了由三个实体组成的简单数据模型。 在本教程中,将添加更多实体和关系,并通过指定格式、验证和数据库映射规则来自定义数据模型。 本文演示了自定义数据模型的两种方法:向实体类添加属性以及向数据库上下文类添加代码。

完成本教程后,实体类将构成下图所示的完整数据模型:

School_class_diagram

在本教程中,你将了解:

  • 自定义数据模型
  • 更新学生实体
  • 创建 Instructor 实体
  • 创建 OfficeAssignment 实体
  • 修改 Course 实体
  • 创建 Department 实体
  • 修改 Enrollment 实体
  • 将代码添加到数据库上下文
  • 使用测试数据设定数据库种子
  • 添加迁移
  • 更新数据库

先决条件

自定义数据模型

本节介绍如何使用指定格式化、验证和数据库映射规则的特性来自定义数据模型。 然后,在以下几个部分中,你将通过向已创建的类添加属性并为模型中的其余实体类型创建新类来创建完整的 School 数据模型。

DataType 属性

对于学生注册日期,目前所有网页都显示有时间和日期,尽管对此字段而言重要的只是日期。 使用数据注释特性,可更改一次代码,修复每个视图中数据的显示格式。 若要查看如何执行此操作,请向 Student 类的 EnrollmentDate 属性添加一个特性。

Models\Student.cs 中,为命名空间添加语句System.ComponentModel.DataAnnotations,并向属性添加usingDataTypeDisplayFormat属性EnrollmentDate,如以下示例所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        public string LastName { get; set; }
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

DataType 属性用于指定比数据库内部类型更具体的数据类型。 在此示例中,我们只想跟踪日期,而不是日期和时间。 DataType 枚举为许多数据类型提供,例如 Date、Time、PhoneNumber、Currency、EmailAddress 等。 应用程序还可通过 DataType 特性自动提供类型特定的功能。 例如,mailto:可以为 DataType.EmailAddress 创建链接,并且可为支持 HTML5 的浏览器中的 DataType.Date 提供日期选择器。 DataType 属性发出 HTML 5 数据(发音的数据短划线)属性,HTML 5 浏览器可以理解这些属性。 DataType 属性不提供任何验证。

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

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

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

ApplyFormatInEditMode 设置指定在文本框中显示值以供编辑时,还应应用指定的格式。 (你可能不希望对某些字段使用,例如,对于货币值,你可能不希望文本框中的货币符号进行编辑。

可以单独使用 DisplayFormat 属性,但通常最好也使用 DataType 属性。 该 DataType 属性传达 数据的语义 ,而不是如何在屏幕上呈现数据,并提供以下好处,你无法获取 DisplayFormat

  • 浏览器可启用 HTML5 功能(例如,显示日历控件、区域设置适用的货币符号、电子邮件链接、某种客户端输入验证等)。
  • 默认情况下,浏览器将根据区域设置使用正确的格式呈现数据。
  • DataType 属性可让 MVC 选择正确的字段模板来呈现数据(DisplayFormat 使用字符串模板)。 有关详细信息,请参阅 Brad Wilson 的 ASP.NET MVC 2 模板。 (虽然为 MVC 2 编写,但本文仍适用于当前版本的 ASP.NET MVC。

如果将 DataType 属性与日期字段一起使用,则还必须指定 DisplayFormat 该属性,以确保该字段在 Chrome 浏览器中正确呈现。 有关详细信息,请参阅 此 StackOverflow 线程

有关如何在 MVC 中处理其他日期格式的详细信息,请转到 MVC 5 简介:检查“编辑方法”和“编辑视图 ”,并在页面中搜索“国际化”。

再次运行“学生索引”页,并注意到不再显示注册日期的时间。 对于使用 Student 模型的任何视图,也是如此。

Students_index_page_with_formatted_date

The StringLengthAttribute

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

假设要确保用户输入的名称不超过 50 个字符。 若要添加此限制,请将 StringLength 属性添加到LastName属性FirstMidName,如以下示例所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime EnrollmentDate { get; set; }
        
        public virtual ICollection<Enrollment> Enrollments { get; set; }
    }
}

StringLength 属性不会阻止用户输入名称的空格。 可以使用 RegularExpression 属性将限制应用于输入。 例如,以下代码要求第一个字符是大写字符,其余字符按字母顺序排列:

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

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

运行应用程序,然后单击“ 学生 ”选项卡。出现以下错误:

创建数据库后,支持“SchoolContext”上下文的模型已更改。 请考虑使用Code First 迁移来更新数据库(https://go.microsoft.com/fwlink/?LinkId=238269)。

数据库模型已更改,需要更改数据库架构,实体框架检测到该更改。 你将使用迁移来更新架构,而无需使用 UI 丢失添加到数据库的任何数据。 如果更改了由Seed该方法创建的数据,则由于在方法中使用Seed了 AddOrUpdate 方法,该数据将更改回其原始状态。 (AddOrUpdate 等效于数据库术语中的“upsert”操作。

在包管理器控制台 (PMC) 中输入以下命令:

add-migration MaxLengthOnNames
update-database

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

实体框架使用前面追加到迁移文件名的时间戳来订购迁移。 可以在运行 update-database 命令之前创建多个迁移,然后按照创建迁移的顺序应用所有迁移。

运行“ 创建 ”页,并输入长度超过 50 个字符的名称。 单击“创建时,客户端验证会显示一条错误消息:字段 LastName 必须是最大长度为 50 的字符串。

列属性

还可使用特性来控制类和属性映射到数据库的方式。 假设在名字字段使用了 FirstMidName,这是因为该字段也可能包含中间名。 但却希望将数据库列命名为 FirstName,因为要针对数据库编写即席查询的用户习惯使用该姓名。 若要进行此映射,可使用 Column 特性。

Column 特性指定,创建数据库时,映射到 FirstMidName 属性的 Student 表的列将被命名为 FirstName。 换言之,在代码引用 Student.FirstMidName 时,数据来源将是 Student 表的 FirstName 列或在其中进行更新。 如果未指定列名,则会为它们提供与属性名称相同的名称。

Student.cs文件中,添加 using System.ComponentModel.DataAnnotations.Schema 的语句,并将列名属性添加到FirstMidName属性,如以下突出显示的代码所示:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [StringLength(50)]       
        public string LastName { get; set; }
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]

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

添加 Column 属性 会更改支持 SchoolContext 的模型,因此它与数据库不匹配。 在 PMC 中输入以下命令以创建另一个迁移:

add-migration ColumnFirstName
update-database

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

下图显示了在应用前两个迁移之前的原始列名称。 除了从列名更改为 FirstMidName FirstName 之外,两个名称列的长度已从 MAX 长度更改为 50 个字符。

显示两个 Student 表的名称和数据类型差异的两个屏幕截图。

还可以使用 Fluent API 进行数据库映射更改,如本教程后面部分所示。

注意

如果尚未按以下各节所述创建所有实体类就尝试进行编译,则可能会出现编译器错误。

更新学生实体

Models\Student.cs中,将前面添加的代码替换为以下代码。 突出显示所作更改。

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace ContosoUniversity.Models
{
    public class Student
    {
        public int ID { get; set; }
        [Required]
        [StringLength(50)]
        [Display(Name = "Last Name")]
        public string LastName { get; set; }
        [Required]
        [StringLength(50, ErrorMessage = "First name cannot be longer than 50 characters.")]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        public string FirstMidName { get; set; }
        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Enrollment Date")]
        public DateTime EnrollmentDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get
            {
                return LastName + ", " + FirstMidName;
            }
        }

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

必需属性

Required 属性 将名称属性指定字段。 Required attribute值类型(如 DateTime、int、double 和 float)不需要此参数。 无法为值类型分配 null 值,因此它们本质上被视为必填字段。

Required 特性必须与 MinimumLength 结合使用才能强制执行 MinimumLength

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

MinimumLengthRequired 允许通过空格来满足验证。 使用属性 RegularExpression 完全控制字符串。

显示属性

Display 特性指定文本框的标题应是“名”、“姓”、“全名”和“注册日期”,而不是每个实例中的属性名称(其中没有分隔单词的空格)。

FullName Calculated 属性

FullName 是计算属性,可返回通过串联两个其他属性创建的值。 因此,它只有一个 get 访问器,并且不会 FullName 在数据库中生成任何列。

创建 Instructor 实体

创建 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 = "Last Name")]
        [StringLength(50)]
        public string LastName { get; set; }

        [Required]
        [Column("FirstName")]
        [Display(Name = "First Name")]
        [StringLength(50)]
        public string FirstMidName { get; set; }

        [DataType(DataType.Date)]
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        [Display(Name = "Hire Date")]
        public DateTime HireDate { get; set; }

        [Display(Name = "Full Name")]
        public string FullName
        {
            get { return LastName + ", " + FirstMidName; }
        }

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

请注意,StudentInstructor 实体中具有几个相同属性。 本系列后面的实现继承教程将重构此代码以消除冗余。

可以将多个属性放在一行上,因此还可以按如下所示编写讲师课程:

public class Instructor
{
   public int ID { get; set; }

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

   [Column("FirstName"),Display(Name = "First Name"),StringLength(50, MinimumLength=1)]
   public string FirstMidName { get; set; }

   [DataType(DataType.Date),Display(Name = "Hire Date"),DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
   public DateTime HireDate { get; set; }

   [Display(Name = "Full Name")]
   public string FullName
   {
      get { return LastName + ", " + FirstMidName; }
   }

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

课程和 OfficeAssignment 导航属性

CoursesOfficeAssignment 是导航属性。 如前所述,它们通常定义为 虚拟 ,以便可以利用名为 延迟加载的 Entity Framework 功能。 此外,如果导航属性可以保存多个实体,则其类型必须实现 ICollection<T> 接口。 例如 ,IList<T> 限定但不 限定 IEnumerable<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 = "Office Location")]
        public string Location { get; set; }

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

生成项目,这会保存更改并验证你尚未进行任何复制和粘贴错误,编译器可以捕获这些错误。

键属性

InstructorOfficeAssignment 实体之间存在一对零或一关系。 办公室分配仅与分配有办公室的讲师相关,因此其主键也是 Instructor 实体的外键。 但 Entity Framework 无法自动识别InstructorID为此实体的主键,因为它的名称不遵循ID类名ID命名约定。 因此,Key 特性用于将其识别为主键:

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

如果实体具有自己的主键,但想要将属性命名为不同于classnameID或不同的ID属性,也可以使用Key该属性。 默认情况下,EF 将密钥视为非数据库生成的,因为该列用于标识关系。

ForeignKey 属性

如果两个实体之间存在一对零或一对一关系,或者两个实体(如 between OfficeAssignmentInstructor)之间存在一对一关系,则 EF 无法确定关系的哪一端是主体,而哪个端是依赖的。 一对一关系在每个类中都有一个引用导航属性,该属性指向另一个类。 ForeignKey 属性可以应用于依赖类来建立关系。 如果省略 ForeignKey 属性,尝试创建迁移时会出现以下错误:

无法确定类型“ContosoUniversity.Models.OfficeAssignment”和“ContosoUniversity.Models.Instructor”之间的关联主体端。 必须使用关系 Fluent API 或数据注释显式配置此关联的主体端。

本教程稍后将介绍如何使用 Fluent API 配置此关系。

讲师导航属性

Instructor 实体具有可以为 null 的 OfficeAssignment 导航属性(因为讲师可能没有办公室分配),并且 OfficeAssignment 该实体具有不可为 null 的 Instructor 导航属性(因为没有讲师的办公室分配不存在 -- InstructorID 不可为 null)。 当实体 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 = "Number")]
      public int CourseID { get; set; }

      [StringLength(50, MinimumLength = 3)]
      public string Title { get; set; }

      [Range(0, 5)]
      public int Credits { get; set; }

      public int DepartmentID { get; set; }

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

课程实体具有指向相关Department实体且具有Department导航属性的外键属性DepartmentID。 如果拥有相关实体的导航属性,则 Entity Framework 不会要求为数据模型添加外键属性。 只要需要外键,EF 就会在数据库中自动创建外键。 但如果数据模型包含外键,则更新会变得更简单、更高效。 例如,提取要编辑的课程实体时,如果未加载该实体,则该 Department 实体为 null,因此在更新课程实体时,必须首先提取 Department 实体。 数据模型中包含外键属性 DepartmentID 时,更新前无需提取 Department 实体。

DatabaseGenerated 属性

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

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

默认情况下,Entity Framework 假定主键值由数据库生成。 大多数情况下,这是理想情况。 但对 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; }
    

创建 Department 实体

使用以下代码创建 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 = "Start Date")]
      public DateTime StartDate { get; set; }

      public int? InstructorID { get; set; }

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

列属性

之前使用 Column 属性 更改列名称映射。 在实体的代码 Department 中,该 Column 属性用于更改 SQL 数据类型映射,以便使用数据库中的 SQL Server 货币 类型定义列:

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

通常不需要列映射,因为 Entity Framework 通常根据为属性定义的 CLR 类型选择适当的 SQL Server 数据类型。 CLR decimal 类型会映射到 SQL Server decimal 类型。 但在这种情况下,你知道该列将持有货币金额,并且 货币 数据类型更适合该列。 有关 CLR 数据类型及其与 SQL Server 数据类型匹配方式的详细信息,请参阅 Entity FrameworkTypes 的 SqlClient。

外键和导航属性

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

  • 一个系可能有也可能没有管理员,而管理员始终是讲师。 因此,该 InstructorID 属性作为实体的外键 Instructor 包含在内,并在类型指定后 int 添加问号以将该属性标记为可为 null。导航属性已命名 Administrator ,但保存实体 Instructor

    public int? InstructorID { get; set; }
    public virtual Instructor Administrator { get; set; }
    
  • 一个部门可能有许多课程,因此有一个 Courses 导航属性:

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

    注意

    按照约定,Entity Framework 能针对不可为 null 的外键和多对多关系启用级联删除。 这可能导致循环级联删除规则,尝试添加迁移时该规则会造成异常。 例如,如果未将 Department.InstructorID 属性定义为可为 null,则会收到以下异常消息:“引用关系将导致不允许循环引用。如果业务规则需要 InstructorID 属性不可为 null,则必须使用以下 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 = "No grade")]
        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; }
    

多对多关系

StudentCourse 实体间存在多对多关系,Enrollment 实体在数据库中充当带有效负载的多对多联接表。 这意味着除了 Enrollment 联接表的外键外,表还包含其他数据(在本例中为主键和 Grade 属性)。

下图显示这些关系在实体关系图中的外观。 (此关系图是使用 /a0> 生成的Entity Framework Power Tools;创建关系图不是本教程的一部分,它只是在此处用作插图。

学生Course_many到many_relationship

每条关系线的一端显示 1,另一端显示星号 (*),这表示一对多关系。

如果 Enrollment 表不包含年级信息,则它只需包含两个外键:CourseIDStudentID。 在这种情况下,它将对应于数据库中没有有效负载(或纯联接表)的多对多联接表,并且根本不需要为其创建模型类。 实体InstructorCourse具有这种多对多关系,正如你所看到的那样,它们之间没有实体类:

讲师Course_many到many_relationship

但是,数据库中需要联接表,如以下数据库关系图中所示:

讲师Course_many到many_relationship_tables

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

实体关系图

下图显示 Entity Framework Power Tools 针对已完成的学校模型创建的关系图。

School_data_model_diagram

除了多对多关系线(* 到 *)和一对多关系线(1 到 *),还可以在此处查看讲师和部门实体之间的InstructorOfficeAssignment一对零或一对多关系线(1 到 0..1)。

将代码添加到数据库上下文

接下来,将新实体添加到 SchoolContext 类,并使用 Fluent API 调用自定义某些映射。 API 是“fluent”,因为它通常通过将一系列方法调用字符串化为单个语句,如以下示例所示:

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

在本教程中,仅对不能对属性执行的数据库映射使用 fluent API。 但 Fluent API 还可用于指定大多数格式化、验证和映射规则,这可通过特性完成。 MinimumLength 等特性不能通过 Fluent API 应用。 如前所述, MinimumLength 不会更改架构,它仅应用客户端和服务器端验证规则

某些开发者倾向于仅使用 Fluent API 来让实体类保持“干净”。如有需要,可混合使用特性和 Fluent API,且有些自定义只能通过 Fluent API 实现,但通常建议选择一种方法并尽可能坚持使用这一种。

若要将新实体添加到数据模型并执行未使用属性执行的数据库映射,请将 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 方法中的新语句配置多对多联接表:

  • 对于实体之间的InstructorCourse多对多关系,代码指定联接表的表和列名。 Code First 可以在不使用此代码的情况下为你配置多对多关系,但如果不调用它,你将获得默认名称,例如 InstructorInstructorIDInstructorID 的默认名称。

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

以下代码提供了一个示例,说明如何使用 fluent API 而不是属性来指定与OfficeAssignment实体之间的关系Instructor

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对象时,使用代码Instructors = new List<Instructor>()将导航属性初始化Instructors为空集合。 这样,就可以使用Instructors.Add该方法添加Instructor与此Course相关的实体。 如果未创建空列表,则无法添加这些关系,因为 Instructors 该属性将为 null 且没有方法 Add 。 还可以将列表初始化添加到构造函数。

添加迁移

在 PMC 中输入 add-migration 命令(尚未执行 update-database 该命令):

add-Migration ComplexDataModel

如果此时尝试运行 update-database 命令(先不要执行此操作),则会出现以下错误:

ALTER TABLE 语句与 FOREIGN KEY 约束“FK_dbo.Course_dbo.Department_DepartmentID”冲突。 冲突发生位置:数据库“ContosoUniversity”、表“dbo.Department”和列“DepartmentID”。

有时,使用现有数据执行迁移时,需要将存根数据插入数据库中以满足外键约束,这就是现在必须执行的操作。 ComplexDataModel Up 方法中生成的代码将不可为 null 的 DepartmentID 外键添加到 Course 表中。 由于代码运行时表中已存在行 Course ,操作将失败, AddColumn 因为 SQL Server 不知道在不能为 null 的列中放置的值。 因此,必须更改代码来为新列提供默认值,并创建名为“Temp”的存根部门来充当默认部门。 因此,在方法运行后Up,现有Course行都与“Temp”部门相关。 可以将它们与方法中的 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 方法代码,以确保 Course 所有行(而不仅仅是早期运行 Seed 方法插入的行)都具有有效 DepartmentID 值,然后再从列中删除默认值并删除“Temp”部门。

更新数据库

编辑 <完时间戳>后_ComplexDataModel.cs 文件,请在 PMC 中输入 update-database 命令以执行迁移。

update-database

注意

迁移数据和更改架构时可能会收到其他错误。 如果遇到无法解决的迁移错误,你可以更改连接字符串中的数据库名称,或删除数据库。 最简单的方法是重命名 Web.config 文件中的数据库。 以下示例显示名称更改为CU_Test:

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

使用新数据库时,没有要迁移的数据,并且 update-database 命令更有可能在没有错误的情况下完成。 有关如何删除数据库的说明,请参阅 如何从 Visual Studio 2012 中删除数据库。

如果失败,可以通过在 PMC 中输入以下命令来尝试重新初始化数据库:

update-database -TargetMigration:0

像之前一样在服务器资源管理器打开数据库,并展开“表”节点以查看已创建所有表。 (如果仍有 服务器资源管理器 从前一次打开,单击“ 刷新 ”按钮。

显示“服务器资源管理器”窗口的屏幕截图。学校上下文下的“表”文件夹处于打开状态。

未为 CourseInstructor 表创建模型类。 如前所述,这是实体之间InstructorCourse多对多关系的联接表。

右键单击该CourseInstructor表,然后选择“显示表数据,以验证它是否具有添加到导航属性的Course.Instructors实体的结果Instructor

Table_data_in_CourseInstructor_table

获取代码

下载已完成的项目

其他资源

可以在 ASP.NET 数据访问 - 建议的资源中找到 指向其他 Entity Framework 资源的链接。

后续步骤

在本教程中,你将了解:

  • 自定义数据模型
  • 更新了 Student 实体
  • 已创建 Instructor 实体
  • 已创建 OfficeAssignment 实体
  • 修改了 Course 实体
  • 创建了 Department 实体
  • 修改了注册实体
  • 向数据库上下文添加了代码
  • 已使用测试数据设定数据库种子
  • 已添加迁移
  • 已更新数据库

转到下一篇文章,了解如何读取和显示 Entity Framework 加载到导航属性的相关数据。