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

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

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

School_class_diagram

在本教程中,你将了解:

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

先决条件

自定义数据模型

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

DataType 属性

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

Models\Student.cs 中,为 System.ComponentModel.DataAnnotations 命名空间添加 using 语句,并将 和 DisplayFormat 属性添加到 DataTypeEnrollmentDate 属性,如以下示例所示:

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 属性添加到 LastNameFirstMidName 属性,如以下示例所示:

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 迁移更新数据库 ()”。

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

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

add-migration MaxLengthOnNames
update-database

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

在迁移文件名前面附加的时间戳由 Entity Framework 用于对迁移进行排序。 可以在运行 update-database 命令之前创建多个迁移,然后按照创建顺序应用所有迁移。

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

列属性

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

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

Student.cs 文件中,为 System.ComponentModel.DataAnnotations.Schema 添加using语句,并将列名属性添加到 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 表打开 Student 表设计器。

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

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

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

注意

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

更新 Student 实体

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 计算属性

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 是导航属性。 如前所述,它们通常定义为 虚拟 ,以便可以利用称为 延迟加载的实体框架功能。 此外,如果导航属性可以容纳多个实体,则其类型必须实现 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; }
    }
}

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

Key 属性

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

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

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

ForeignKey 属性

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

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

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

Instructor 导航属性

实体 Instructor (具有可以为 null 的 OfficeAssignment 导航属性,因为讲师可能没有办公室分配) ,并且 OfficeAssignment 该实体具有不可为 null 的 Instructor 导航属性 (因为没有讲师就不能存在办公室分配 - InstructorID 是不可为 null 的) 。 当实体 Instructor 具有相关 OfficeAssignment 实体时,每个实体在其导航属性中都有对另一个实体的引用。

可以在 Instructor 导航属性上放置一个 [Required] 属性来指定必须有相关的讲师,但你不必这样做,因为 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实体的外键属性DepartmentID,并且具有Department导航属性。 如果拥有相关实体的导航属性,则 Entity Framework 不会要求为数据模型添加外键属性。 EF 会在数据库中根据需要自动创建外键。 但如果数据模型包含外键,则更新会变得更简单、更高效。 例如,提取要编辑的课程实体时, Department 如果未加载该实体,则该实体为 null,因此,更新课程实体时,必须先提取该 Department 实体。 数据模型中包含外键属性 DepartmentID 时,更新前无需提取 Department 实体。

DatabaseGenerated 属性

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

[DatabaseGenerated(DatabaseGeneratedOption.None)]
[Display(Name = "Number")]
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; }
    

创建 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 属性 来更改列名映射。 在实体的代码中DepartmentColumn 属性用于更改 SQL 数据类型映射,以便使用数据库中的 SQL Server money 类型定义列:

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

通常不需要列映射,因为实体框架通常根据为属性定义的 CLR 类型选择适当的SQL Server数据类型。 CLR decimal 类型会映射到 SQL Server decimal 类型。 但在这种情况下,你知道列将包含货币金额,并且 money 数据类型更适合这一点。 有关 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 属性) 。

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

学生Course_many到many_relationship

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

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

讲师Course_many到many_relationship

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

讲师Course_many到many_relationship_tables

实体框架会自动创建 CourseInstructor 表,通过读取和更新 和 导航属性来间接读取和更新 Instructor.CoursesCourse.Instructors

实体关系图

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

School_data_model_diagram

除了多对多关系线 (* 到 *) ,一对多关系线 (1 到 *) , 可在此处查看和 实体之间的一对零或一关系线 (1 到 0..1) ,以及OfficeAssignment讲师和部门实体之间的Instructor零或一对多关系线 (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 方法中的新语句配置多对多联接表:

  • 对于 和 Course 实体之间的Instructor多对多关系,代码指定联接表的表名和列名。 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 将它们与正确的部门相关联。

<编辑 timestamp>_ComplexDataModel.cs 文件,注释掉将 DepartmentID 列添加到 Course 表的代码行,并添加以下突出显示的代码 (注释行也突出显示) :

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”部门。

更新数据库

编辑<完 timestamp>_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 表创建模型类。 如前所述,这是 和 Course 实体之间的Instructor多对多关系的联接表。

右键单击该CourseInstructor表并选择“显示表数据”,验证它是否包含你添加到导航属性中的InstructorCourse.Instructors实体的数据。

Table_data_in_CourseInstructor_table

获取代码

下载已完成项目

其他资源

可以在 ASP.NET 数据访问 - 推荐资源中找到指向其他实体框架资源的链接。

后续步骤

在本教程中,你将了解:

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

请继续阅读下一篇文章,了解如何读取和显示实体框架加载到导航属性中的相关数据。