[渣译文] 使用 MVC 5 的 EF6 Code First 入门 系列:为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 Applicationweb

译文版权全部,谢绝全文转载——但您能够在您的网站上添加到该教程的连接。正则表达式

在以前的教程中您已经建立了由三个实体组成的简单的数据模型。在本教程中,您将会添加更多的实体和关系,您会进一步定制数据模型,包括指定格式、验证和数据库映射规则。您会看到两种自定义数据模型的方法:经过将属性添加到实体类和经过将代码添加到数据库上下文类。chrome

当您完成时,实体类将组成一个完整的数据模型,以下图所示:数据库

 

使用特性来定制数据模型

在本节中您会看到如何使用特性来定制数据模型的属性来用于指定格式、验证和数据库映射规则。在余下的章节中您将经过向已经建立的类中添加特性及建立剩余实体的新类来完善School数据模型。api

数据类型特性

对于学生注册日期,虽然您仅仅关心该字段中的日期,但在全部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的默认格式。mvc

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平台及工具团队的高级程序员,做家。

相关文章
相关标签/搜索