[翻译][MVC 5 + EF 6] 6:创建更复杂的数据模型

原文:Creating a More Complex Data Model for an ASP.NET MVC Application

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

  当我们完成后,实体类将完成下图所示的数据模型:

1.通过使用属性来自定义数据模型

1.1.DateType属性:

  修改Models\Student.cs

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

  DateType属性用于指定比数据库内在类型更具体的类型。在上面的例子中,我们只希望追踪日期,而不是日期和时间。DateTeyp枚举类型提供了许多数据类型,比如Date, Time, PhoneNumber, Currency, EmailAddress等。DateType属性还可以使应用程序能够自动提供特定类型的功能。比如,一个mailto:链接可以被创建为DataType.EmailAddress,可以为一个日期选择器提供HTML 5支持的DataType.Date类型。DateType属性向HTML 5添加data-(发音是data dash)属性,这个属性可以被HTML 5浏览器识别。DateType属性不提供任何验证。

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

  DisplayFormat属性用来显示地指定日期格式:

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

  ApplyFormatInEditMode指定设定的格式同样应用于编辑时的显示格式(有时我们也需要将其设置为false,比如货币值,我们编辑时不希望出现货币符号)。

  我们可以只添加DisplayFormat属性,但是一般情况下最好也要把DataType属性同时加上。DataType属性传递数据的语义而不是如何呈现在屏幕上,具有DisplayFormat属性不具备的以下优点:

  • 浏览器可以支持HTML5特性(比如显示日历控件,区域货币符号,电子邮件链接,一些客户端输入验证等)。
  • 默认情况下,浏览器将基于我们的区域设置显示呈现正确的数据格式。
  • DataType属性可以让MVC选择正确的字段模板来呈现数据(DisplayFormat使用字符串模板)。更多信息请查看:ASP.NET MVC 2 Templates

  当我们对一个日期字段使用DataType属性时,我们还必须指定DisplayFormat属性以确保字段在Chrome浏览器中正确呈现。更多信息请查看:this StackOverflow thread

  关于如何在MVC中处理其他的日期格式的更多信息,请查看MVC 5 Introduction: Examining the Edit Methods and Edit View,并在该页面搜索“internationalization”。

  运行程序在Index页面,可以看到日期格式发生改变:

1.2.StringLength属性:

  我们同时可以使用属性来指定数据的验证规则和验证错误信息。StringLength属性可以设置数据库中字段的最大长度,同时为ASP.NET MVC提供客户端和服务端验证。我们也可以指定字符串的最短长度,但是最短长度值不会对数据库结构产生影响。

  修改Models\Student.cs

    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属性类似的功能,但是不会产生客户端验证。

  运行程序,点击Students标签,程序将会报错:

  The model backing the ‘SchoolContext‘ context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269).

  数据库模型已经发生改变,因此需要修改数据库结构,并且EF检测到了这种改变。我们使用迁移更新数据库时,我们通过UI添加到数据库中的数据不会丢失。但是Seed方法产生的数据会变回原来的值,因为我们在Seed方法中使用的是AddOrUpdate(AddOrUpdate与数据库术语“upsert”等价)。

  在Package Manager Console输入以下命令:

add-migration MaxLengthOnNames
update-database

  add-migration命令产生一个名为<timeStamp>_MaxLengthOnNames.cs的文件。该文件包含的Up方法将会更新数据库与现在的数据模型匹配。update-database命令则执行产生的代码。

  迁移文件夹名字前面的时间戳被EF用来排定迁移顺序。我们可以在执行update-database命令前创建多个迁移,迁移将会按照我们创建的顺序依次执行。

  运行Create页面,输入名字超过50字符,点击Create,客户端验证将会显示错误提示:

1.3.Column属性:

  我们同样可以使用属性来控制类和属性如何映射到数据库。加入我们把first-name列命名为FirstMidName,因为该列可能同时包含middle name。但是我们却希望在数据库中该列的名字为FirstName,因为专门编写查询的用户比较习惯这个名字。对于这种映射,我们可以使用Column属性。

  修改Models\Student.cs

    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属性后,数据库与模型变得不匹配,在Package Manager Console输入命令:

add-migration ColumnFirstName
update-database

  在Server Explorer,双击Student表打开表设计器。

  下面的图片是前两次迁移之前的表。除了表名的改变外,name列的类型由Max变为50:

  我们也可以使用Fluent API改变数据库映射,在稍后的教程中将会讲到。

2.完成Student实体的修改

  修改Models\Student.cs

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

2.1.Required属性:

  Required属性,表示值不能为空。我们可以使用StringLength的最小长度来替代Required属性:

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

2.2.Display属性:

  Display属性指定该列在文本框中显示的内容。

2.3.Calculated属性(与前面的属性不同,这里的属性指的是类的属性):

  FullName是Calculated属性,它由其他属性产生。因为该属性只有get访问器,所以FullName不会在数据库中产生一列。

3.创建Instructor实体

  创建Models\Instructor.cs

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

  我们会发现Instructor和Student有几个属性是相同的。在后续的教程中我们将会重构代码消除冗余。

  多个属性可以放在同一行,因此也可以这样:

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

  如果导航属性可以包含多个实体,它的类型必须实现ICollection<T>接口,例如IList<T>是允许的,但是IEnumerable<T>是不允许的,因为IEnumerable<T>没有实现Add。

4.创建OfficeAssignment实体

  新建Models\OfficeAssignment.cs

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

4.1.Key属性:

  Instructor和OfficeAssignment是1对0..1关系。一个OfficeAssignment只存在于被分配的Instructor中,因此它的主键同时也是Instructor的外键。但是EF不能自动将InstructorID识别为OfficeAssignment的主键,因为它没有遵从ID或classnameID的命名约定。Key属性用来标识它是主键。

  如果一个实体确实有主键但是我们没有把它命名为ID或classnameID形式,那么我们就可以使用Key属性。默认情况下EF会把这个主键当作不是数据库产生的,因为该列是用来标识关系的。

4.2.ForeignKey属性:

  当两个实体间是1对0..1或者1对1关系时,EF不能确定哪个是本体哪个是依赖。1对1关系中每个类中都会有另一个类的导航属性。ForeignKey属性应用于依赖类来确定关系。如果我们漏掉了ForeignKey属性,在我们迁移时将会有如下错误:

  Unable to determine the principal end of an association between the types ‘ContosoUniversity.Models.OfficeAssignment‘ and ‘ContosoUniversity.Models.Instructor‘. The principal end of this association must be explicitly configured using either the relationship fluent API or data annotations.

  稍后我们将会学习如何使用fluent API来配置这种关系。

5.修改Course实体

  修改Models\Course.cs

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

  course实体拥有DepartmentID外键属性,并且有Department导航属性。当有相关实体的导航属性时,我们就可以不给数据模型添加ForeignKey属性。EF会在需要的时候自动在数据中添加啊外键。例如,当我们取得一个course实体并编辑它的时候,如果我们不加载Department实体,它将会是空的,因此当我们更新course实体时,我们必须首先获取Department实体。当外键属性DepartmentID包含在数据模型中时,我们在更新之前就不用获取Department。

5.1.DatabaseGenerated属性:

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

6.新建Department实体

  创建Models\Department.cs

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

6.1.Column属性:

  之前我们使用Column属性改变列名映射。在上面的代码中,Column属性被用来改变SQL数据类型映射,这样该列会被定义成SQL的money类型。

  一般情况下列映射是不需要的,因为EF通常会根据我们为属性定义的CLR类型选择适当的SQL Server数据类型。例如,CLR的decimal类型对应SQL Server类型。但是在本例中,该列表示货币,因此money类型更合适。关于CLR数据类型以及它们如何与SQL Server数据类型匹配的更多信息请参考:SqlClient for Entity FrameworkTypes

  按照惯例,EF对非空外键和多对多关系支持级联删除。这可能会导致循环级联删除,当我们在迁移时可能会引起一场。例如,如果我们没有定义Department.InstructorID是可空的,我们将会获得如下异常信息:The referential relationship will result in a cyclical reference that‘s not allowed。如果我们的业务规则要求InstructorID非空,我们必须使用下面的fluent API语句来禁用级联删除:

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

7.修改Enrollment实体

  修改Models\Enrollment.cs

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

  Enrollment在数据库中是Student和Course的中间表,如果Enrollment表没有包含Grade信息,那么我们就可以不创建Enrollment。就像Instructor和Course实体也是多对多关系,但是它们没有中间实体。

  EF会自动在数据库中创建CourseInstructor表,我们可以使用Instructor.CoursesCourse.Instructors导航属性直接读取和更新这个表。

8.用实体关系图显示关系

  下面的图显示了EF Power Tools创建的完整的School模型框架:

9.在数据库上下文添加自定义数据模型的代码

  下面,我们将会在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,这样会保持实体类的“clean”。我们可以将属性和fluent API混合使用,虽然有一些自定义设置只能通过fluent来实现,但是一般情况下推荐的做法是选择两者其中之一然后尽量保持一致性的使用。

  修改DAL\SchoolContext.cs的代码来添加实体到数据模型并且不使用属性来完成数据库映射:

   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可以自动为我们配置多对多关系,但是如果没有这些代码,Code First产生的是默认的名字,例如InstructorID列的名字可能会是InstructorInstructorID

  下面的代码提供了我们如何使用fluent API取代属性来指定Instructor和OfficeAssignment实体关系的例子:

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

  更多关于Fluent API的信息请查看:Fluent API

10.添加测试数据

  修改Migrations\Configuration.cs

    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导航属性。这样可以使用Instructors.Add添加与该Course相关的Instructor实体。如果我们没有创建空的列表,我们将不能添加这些关系,因为Instructors属性是空的,不能使用Add方法。我们也可以列表的把初始化放在构造函数里面。

11.添加迁移并更新数据库

  在PMC中输入add-migration命令(暂时不要输入update-database命令):

add-Migration ComplexDataModel

  如果我们现在运行update-database命令,将会得到如下错误:

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_dbo.Course_dbo.Department_DepartmentID". The conflict occurred in database "ContosoUniversity", table "dbo.Department", column ‘DepartmentID‘.

  有时当我们对已经存在的数据执行迁移时,我们需要将存根数据插入到数据库,以满足外键约束,这是我们现在必须做的工作。在ComplexDataModel中Up方法Course为添加了非空外键DepartmentID。因为当插入测试数据时,Course表中已存在插入的数据,因此AddColumn操作将会失败,因为SQL Server不知道该列应该插入什么样的非空值。因此必须修改代码给新增的列指定默认值,并且插入一条名为“Temp”的存根department数据,来作为默认的department。这样,已经存在的Course行数据在Up方法运行后将会与“Temp”department关联。

  修改<timestamp>_ComplexDataModel.cs文件:

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

  在PMC输入命令:

update-database

  说明:

    在迁移数据和改变数据库架构时可能还会遇到其他错误。如果遇到我们无法解决的迁移错误,我们可以修改连接字符串的数据库名或者删除数据库。最简单的做法是修改数据库名,例如下面:

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

    关于如何删除数据库请查看:How to Drop a Database from Visual Studio 2012

    如果重命名数据库依然失败,另一个方法是使用下面命令重新初始化数据库:

update-database -TargetMigration:0

  打开Server Explorer查看数据表:

  查看CourseInstructor表中的数据:

时间: 2024-11-16 09:39:04

[翻译][MVC 5 + EF 6] 6:创建更复杂的数据模型的相关文章

MVC、EF中,创建更加复杂的数据模型

在前面的课程中,你已经创建了一个简单的由三个实体组成的数据模型.在这个课程中,你将要增加更多的实体,以及关系,使用数据标注特性来控制模型类的行为. 在完成的时候,实体类表示的完整数据模型如下所示: 4-1 使用特性控制格式.验证以及数据库映射 在这一节中,你将会看到如何使用特性来控制数据模型的格式化.验证以及数据库映射.然后在后继的节中,将要通过为已经创建的类.新创建的类增加特性,来创建完整的 School 数据模型. 4-1-1 DisplayFormat 特性 对于学生的注册日期来说,虽然你

[渣译文] 使用 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 Application 译文版权所有,谢绝全文转载--但您可以在您的网站上添加到该教程的链接. 在之前的教程中您已经创建了由三个实体组成的简单的数据模型.在本教程中

[翻译][MVC 5 + EF 6] 7:加载相关数据

原文:Reading Related Data with the Entity Framework in an ASP.NET MVC Application 1.延迟(Lazy)加载.预先(Eager)加载.显式(Explicit)加载: EF加载相关数据到实体导航属性有以下几种方式: 延迟加载:当实体第一次读取时,相关数据没有加载.当第一次试图访问导航属性时,所需的导航数据自动加载.这导致多条查询语句被发送到数据库:一条查询实体本身,一条查询实体相关数据.DbContext类默认启用延迟加载

EntityFramework_MVC4中EF5 新手入门教程之四 ---4.在EF中创建更复杂的数据模型

在以前的教程你曾与一个简单的数据模型,由三个实体组成.在本教程中,您将添加更多的实体和关系,并通过指定格式. 验证和数据库映射规则,您将自定义数据模型.你会看到自定义的数据模型的两种方式: 通过添加属性,实体类并通过将代码添加到数据库上下文类. 当您完成时,实体类将已完成的数据模型中,如下图所示: 通过使用属性进行自定义的数据模型 在本节中,您会看到如何通过使用指定的格式,验证和数据库映射规则的属性来自定义数据模型.然后在以下各节,您将创建的几个完整的School数据模型,通过添加属性的类已创建

[翻译][MVC 5 + EF 6] 5:Code First数据库迁移与程序部署

原文:Code First Migrations and Deployment with the Entity Framework in an ASP.NET MVC Application 1.启用Code First迁移: 当我们开发一个新的程序时,数据模型经常会发生改变,每次模型发生改变时,就会变得与数据库不同步.我们之前配置EF在每次数据模型发生改变时自动删除然后重建数据库.当我们增加.删除或者改变实体类或者改变DbContext类时,在程序下次运行时将会自动删除已经存在的数据库,并且创

[翻译][MVC 5 + EF 6] 11:实现继承

原文:Implementing Inheritance with the Entity Framework 6 in an ASP.NET MVC 5 Application 1.选择继承映射到数据库表: 在School数据模型里面,Instructor和Student类有几个属性是相同的: 假设我们想要消除Instructor和Student实体属性的冗余代码.或者我们想要编写一个可以格式化name的服务,而不用考虑这个name是来自一个instructor还是student.我们可以创建一个

[翻译][MVC 5 + EF 6] 9:异步和存储过程

原文:Async and Stored Procedures with the Entity Framework in an ASP.NET MVC Application 1.为什么使用异步代码: 一个服务器可用的线程数量是有限的,在高负载的情况下所有的可用线程都可能在使用.在这种情况下,在线程被释放之前服务器不能处理新的请求.在同步代码中,很多线程被占用但是实际上没有做任何操作,因为它们在等待I/O完成.在异步代码中,当一个进程在等待I/O完成的过程中,它的线程将会被释放用于处理其他请求.这

[翻译][MVC 5 + EF 6] 4:弹性连接和命令拦截(Command Interception)

原文:Connection Resiliency and Command Interception with the Entity Framework in an ASP.NET MVC Application [注:本节教程可以选择性学习] 本节教程将学习EF6的两个重要特性,这两个特性在我们将程序部署在云环境时特别有用: 弹性连接(connection resiliency):遇到瞬时的连接错误时自动重试连接. 命令拦截(command interception):捕获所有发送到数据库的查询

[翻译][MVC 5 + EF 6] 2:基础的增删改查(CRUD)

原文:Implementing Basic CRUD Functionality with the Entity Framework in ASP.NET MVC Application 1.修改Views\Student\Details.cshtml: @model ContosoUniversity.Models.Student @{ ViewBag.Title = "Details"; } <h2>Details</h2> <div> <