zoukankan      html  css  js  c++  java
  • MVC5与EF6结合教程(06):创建更复杂的数据模型

    原文:https://docs.microsoft.com/zh-cn/aspnet/mvc/overview/getting-started/getting-started-with-ef-using-mvc/creating-a-more-complex-data-model-for-an-asp-net-mvc-application

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

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

    School_class_diagram

    在本教程中,你将了解:

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

    一、系统必备

    二、自定义数据模型

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

    1、DataType 特性

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

    ModelsStudent.cs中,添加 System.ComponentModel.DataAnnotations 命名空间的 using 语句,并将 DataTypeDisplayFormat 特性添加到 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 枚举提供多种数据类型,例如日期、时间、PhoneNumber、货币、EmailAddress等。 应用程序还可通过 DataType 特性自动提供类型特定的功能。 例如,可以为EmailAddress创建 mailto: 链接,并且可以在支持HTML5的浏览器中为数据类型提供日期选择器。 数据类型属性发出 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

    2、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 迁移更新数据库(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的字符串。

    3、列属性

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

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

    Student.cs文件中,添加system.componentmodelusing 语句,并将列名特性添加到 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; }
        }
    }
    

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

    add-migration ColumnFirstName
    update-database
    

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

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

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

    Note

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

    三、更新学生实体

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

    1、必需的属性

    必需的属性使名称属性成为必填字段。 值类型(例如 DateTime、int、double 和 float)不需要 Required attribute 值类型不能赋予 null 值,因此它们原本被视为必填字段。

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

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

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

    2、显示属性

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

    3、FullName 计算属性

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

    四、创建 Instructor 实体

    创建ModelsInstructor.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; }
    }
    

    1、课程和 OfficeAssignment 导航属性

    CoursesOfficeAssignment 是导航属性。 如前所述,它们通常定义为虚拟,以便能够利用称为延迟加载的实体框架功能。 此外,如果导航属性可以包含多个实体,则其类型必须实现ICollection<t>接口。 例如, IList<t>限定,而不是IEnumerable<t> ,因为 IEnumerable<T> 不实现Add

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

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

    我们的业务规则陈述,一个指导员最多只能有一个办公室,因此 OfficeAssignment 定义为单个 OfficeAssignment 实体(如果没有分配任何 office,可能 null 此实体)。

    public virtual OfficeAssignment OfficeAssignment { get; set; }
    

    五、创建 OfficeAssignment 实体

    用以下代码创建ModelsOfficeAssignment.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; }
        }
    }
    

    生成项目,该项目保存更改并验证编译器能否捕获任何复制和粘贴错误。

    1、键属性

    InstructorOfficeAssignment 实体之间存在一对零或一关系。 Office 分配只与分配给它的指导员相关,因此,其主键也是 Instructor 实体的外键。 但实体框架无法自动将 InstructorID 视为此实体的主键,因为其名称不遵循 IDclassname ID 命名约定。 因此,Key 特性用于将其识别为主键:

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

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

    2、ForeignKey 特性

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

    无法确定类型为 "ContosoUniversity" 和 "ContosoUniversity" 之间的关联的主体端。必须使用关系 Fluent API 或数据批注显式配置此关联的主体端。

    稍后在本教程中,你将了解如何配置此关系与 Fluent API。

    3、讲师导航属性

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

    您可以在讲师导航属性中放置一个 [Required] 特性,以指定必须有相关的指导员,但您不必这样做,因为 InstructorID 外键(也是此表的键)不可为 null。

    六、修改课程实体

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

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

    1、DatabaseGenerated 特性

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

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

    默认情况下,实体框架假定数据库生成主键值。 大多数情况下,这是理想情况。 但是,对于 Course 实体,你将使用用户指定的课程编号,如一个部门的1000系列、另一个部门的2000系列等等。

    2、外键和导航属性

    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 实体

    用以下代码创建ModelsDepartment.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; }
       }
    }
    

    1、列属性

    之前,你已使用列属性更改列名映射。 Department 实体的代码中,Column 特性用于更改 SQL 数据类型映射,以便使用数据库中的 SQL Server money类型定义列:

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

    通常不需要列映射,因为实体框架通常基于您为属性定义的 CLR 类型选择适当的 SQL Server 数据类型。 CLR decimal 类型会映射到 SQL Server decimal 类型。 但在这种情况下,您知道列将包含货币金额,而money数据类型则更适合这样做。 有关 CLR 数据类型以及它们如何与 SQL Server 数据类型匹配的详细信息,请参阅SqlClient For Entity sqlclient 类型

    2、外键和导航属性

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

    • 一个系可能有也可能没有管理员,而管理员始终是讲师。 因此,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; }
      

      Note

      按照约定,Entity Framework 能针对不可为 null 的外键和多对多关系启用级联删除。 这可能导致循环级联删除规则,尝试添加迁移时该规则会造成异常。 例如,如果未将 Department.InstructorID 属性定义为可为 null,则会收到以下异常消息: "引用关系将导致循环引用,这是不允许的。" 如果业务规则要求 InstructorID 属性为不可为 null,则必须使用以下 Fluent API 语句禁用关系的级联删除:

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

    八、修改 Enrollment 实体

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

    1、外键和导航属性

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

    • 注册记录面向一门课程,因此存在 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; }
      

    2、多对多关系

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

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

    Student-Course_many-to-many_relationship

    每个关系线一端有1个,另一个用星号(*)表示一对多关系。

    如果 Enrollment 表未包括评分信息,只需包含两个外键 CourseIDStudentID 在这种情况下,它与数据库中没有负载(或纯联接表)的多对多联接表相对应,无需为其创建模型类。 InstructorCourse 实体具有这种类型的多对多关系,如您所见,它们之间没有实体类:

    Instructor-Course_many-to-many_relationship

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

    Instructor-Course_many-to-many_relationship_tables

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

    九、实体关系图

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

    School_data_model_diagram

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

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

    接下来,将新实体添加到 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 还可用于指定大多数格式化、验证和映射规则,这可通过特性完成。 MinimumLength 等特性不能通过 Fluent API 应用。 如前文所述,MinimumLength 不会更改架构,它只应用客户端和服务器端验证规则。

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

    若要将新实体添加到数据模型,并使用属性执行你不执行的数据库映射,请将DALSchoolContext.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 可以为你配置多对多关系,而无需此代码,但如果不调用此代码,则将获取 InstructorID 列的默认名称,如 InstructorInstructorID

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

    下面的代码提供了一个示例,说明如何使用 Fluent API 而不是属性来指定 InstructorOfficeAssignment 实体之间的关系:

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

    有关 "Fluent API" 语句在幕后执行的操作的信息,请参阅 "流畅 API " 博客文章。

    十一、使用测试数据设定数据库种子

    MigrationsConfiguration.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));
            }
        }
    }
    

    正如你在第一个教程中看到的那样,此代码中的大多数只是更新或创建新的实体对象,并根据测试需要将示例数据加载到属性中。 不过,请注意如何处理与 Instructor 实体具有多对多关系的 Course 实体:

    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 方法添加与此 Course 相关的 Instructor 实体。 如果未创建空列表,则无法添加这些关系,因为 Instructors 属性为 null,并且没有 Add 方法。 你还可以将列表初始化添加到构造函数。

    十二、添加迁移

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

    add-Migration ComplexDataModel

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

    ALTER TABLE 语句与外键约束 "FK_dbo 冲突。当然_dbo。部门_DepartmentID "。在表 "dbo" 的数据库 "ContosoUniversity" 中发生冲突。部门 ",列" DepartmentID "。

    有时,当您使用现有数据执行迁移时,您需要将存根(stub)数据插入到数据库中以满足外键约束,这就是您现在必须执行的操作。 ComplexDataModel Up 方法中生成的代码将不可以为 null 的 DepartmentID 外键添加到 Course 表中。 由于在代码运行时 Course 表中已有行,因此 AddColumn 操作将失败,因为 SQL Server 不知道要放入列中的值不能为 null。 因此,必须更改代码以为新列指定默认值,并创建一个名为 "Temp" 的存根部作为默认部门。 因此,在 Up 方法运行后,现有 Course 行将与 "Temp" 部门相关。 您可以将它们关联到 Seed 方法中的正确部门。

    编辑 _ComplexDataModel.cs 文件>的 <时间戳,注释掉将 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 方法所插入的行)在从列中删除默认值并删除 "Temp" 部门之前都具有有效的 DepartmentID 值。

    十三、更新数据库

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

    update-database
    

    Note

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

    <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 数据访问-建议的资源中找到指向其他实体框架资源的链接。

    十六、后续步骤

    在本教程中,你将了解:

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

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

  • 相关阅读:
    你不知道的javascript -- 数据类型
    draft.js开发富文本编辑器
    webpack4配置react开发环境
    使用yarn代替npm
    promise基础和进阶
    express route的写法
    理解es6箭头函数
    Mocha测试
    js 实现继承
    Unity3D使用经验总结 缺点篇
  • 原文地址:https://www.cnblogs.com/springsnow/p/13264882.html
Copyright © 2011-2022 走看看