EF6学习笔记总目录:ASP.NET MVC5 及 EF6 学习笔记 - (目录整理)
本篇原文地址:Creating a More Complex Data Model
本篇讲的比较碎,很多内容本人认为并不是EF的内容,既然原文讲了,那就按照原文来学习吧。。。
第1步:通过属性来定制化数据模型
通过 DataType Attribute来定义数据模型字段属性:
先加申明 using System.ComponentModel.DataAnnotations;
然后为需要定义属性的字段加上属性:(例如EnrollmentDate 加上日期DataType,以及显示格式的属性)
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace EFTest.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 Enumeration链接来查看;主要有用的是:Date,Time,EmailAddress,Password等;
通过DataType定义的数据属性,在应用中得到特殊的数据处理功能,比如Email的话,就在显示的时候自动变为点击发Email的Link;如果Date的话,会在前端自动显示为一个日期输入选择框(在HTML5的情况下);
但是DataType不提供验证功能,并且也不特别指定显示格式,默认的显示格式基于服务器的CultureInfo
DispalyFormat属性定义就是用来明确定义显示格式;
DisplayFormat可以单独使用,但建议是配合DataType一起使用,配合使用会有以下优点:
1、HTML5支持的浏览器可以根据DataType做出一些特定的调整及显示(比如会显示计算器、日期输入框、Email地址链接等等)
2、默认情况下,浏览器会根据客户端计算机的本地化显示设置来进行渲染(比如本地化的日期时间格式)
3、DataType属性可以让MVC选择正确的字段类型去渲染数据,DisplayFormat使用字符串类型模板;
更多的信息可以参考:MVC 5 Introduction: Examining the Edit Methods and Edit View
通过StringLength 属性定义长度
可以通过StringLength这个属性来定义一个字段的最长长度,并且可以设置出现错误后应该提供的错误信息;
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace EFTest.Models { public class Student { public int ID { get; set; } [StringLength(50)] public string LastName { get; set; } [StringLength(50, ErrorMessage = "First name 不可以超过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; } } }
比如设定LastName、FirstMidName最长只能50个字符,并且在FirstMidName输入超过50个字符的时候提示相应错误;
另外,StringLength属性定义不能防止用户输入空格,所以可以通过RegularExpression来进行限制:
[RegularExpression(@"^[A-Z]+[a-zA-Z''-'s]*$")]
以上这个属性定义,限制首字母为大写,后面也必须为字母;
PS: MaxLength属性定义也能定义字段最大长度,但是并不提供客户端验证。
这个时候如果直接运行应用,并点击Students页面,就会有以下错误:
说明数据模型变了,数据库也需要先升级才可以;
先在PM控制台执行 add-migration MaxLengthOnNames
会自动创建一个带时间戳的数据升级文件;然后在PM控制台继续输入:update-database
完成后,就可以再次尝试进入应用的Student页面,并点击Create进入新建页面:
Column 属性定义:当一些特定情况下,希望数据库里的字段名采用一个约定格式名,而模型中是另一个名称,则需要通过Column来强制换个名字:
首先,添加申明:using System.ComponentModel.DataAnnotations.Schema;
然后用[Column("NewColumnName")] 定义数据库中的列名:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.Models { public class Student { public int ID { get; set; } [StringLength(50)] public string LastName { get; set; } [StringLength(50, ErrorMessage = "First name 不可以超过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; } } }
当然,在做了这个改动后,需要今天数据库升级操作:
在PM控制台输入以下命令:
add-migration ColumnFirstName
update-database
更新后,列名变了,并且DataType从nvarchar(MAX) 变为 nvarchar(50):
PS: 这个操作可以通过 Fluent API 来完成,后面也会提及到。
将Student模型变更为以下完整版本:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.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 不可超过50个字符.")] [Column("FirstName")] [Display(Name = "First Name")] public string FirstMidName { get; set; } [DataType(DataType.Date)] [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)] [Display(Name = "Enrollment Date")] public DateTime EnrollmentDate { get; set; } [Display(Name = "Full Name")] public string FullName { get { return LastName + ", " + FirstMidName; } } public virtual ICollection<Enrollment> Enrollments { get; set; } } }
[Required]对于值类型的是不需要的,例如int , DateTime, double, float ,因为这些值本身是不可以为空NULL的;
对于字符串类型的属性(property),可以通过设置最小长度来代替[Required]:
[Display(Name = "Last Name")] [StringLength(50, MinimumLength=1)] public string LastName { get; set; }
Display定义在显示View的时候,对应该属性(property)的输入框标题内容;
另外,FullName是一个计算出来的属性(property),所以只有get ,没有set ,这样在数据库里是不会增加 FullName这个列的;
新建Instructor模型:(和Student 模型差不多)
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.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; } } }
当然也可以简化属性定义为一行:(通过逗号来分割)
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.Models { 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; } } }
在Instructor模型中 Courses 和 OfficeAssignment为导航属性,采用virtual来定义为延迟加载;
Instructor对应Courses 是1对n ,所以采用ICollection<T>来表示;
Instructor对应OfficeAssignment是1对1,所以直接采用OfficeAssignment来定义;
新建OfficeAssignment模型:
using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.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; } } }
Instructor对应OfficeAssignment是1对1或者 1对0,即OfficeAssignment表中的InstructorID一定是在Instructor存在这个ID的;
所以对于OfficeAssignment InstructorID
需要设置为主键,并且要定义为从Instructor ID来的外键;
修改Course模型
using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.Models { public class Course { [DatabaseGenerated(DatabaseGeneratedOption.None)] [Display(Name = "Number")] public int CourseID { get; set; } [StringLength(50, MinimumLength = 3)] public string Title { get; set; } [Range(0, 5)] public int Credits { get; set; } public int DepartmentID { get; set; } public virtual Department Department { get; set; } public virtual ICollection<Enrollment> Enrollments { get; set; } public virtual ICollection<Instructor> Instructors { get; set; } } }
新建Department模型
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace EFTest.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、模型中Properties如果名称为ID或者是类名+ID,则EF会自动将其变为主键;
2、如果主键为int型,默认会定义为数据库自增长的类型,除非用 [DatabaseGenerated(DatabaseGeneratedOption.None)] 来明确告知不要;
3、如果是1对1或0的对应关系,需要明确用[ForeignKey("XXXX")]来告知EF哪个是主表哪个是子表,不然会报异常;
4、如果是多对1或0的对应关系,如果Properties是值类型,则需要用 问号 来定义可以为空;
5、默认情况下,EF是开启联级删除(cascade delete),那么如果外键导致的联级删除出现多个
例如: A -> B 是 1对多, A -> C 也是 1对多, B 和 C 是多对多, 那么要删除 一个A,就需要联级删除先删除 B 和 C ,而要删除B 和 C 就要先删除 B 和 C 连接对应;
这样就有两条路都到删除 B 和 C 的连接关系,这个是数据库所不允许的;
那么只有两种方式:
a) fluent API来申明关闭这个A的cascade delete;
b) 把A -> B设置为 0,1对多,或者 A -> C 设置为 0,1对多, 即删除A时,不是删除B,而是把B里涉及到A的外键列值设置为NULL。
6、如果是多对多的关系,则EF会自动新建一个表来存储多对多的关系;
在上下文类中通过Fluent API来定义复杂模型关系及一些设定
在上下文类中增加定义所有的实体,并通过Fluent API来定义一些设定;
Fluent API可以用来设定绝大部分的模型关系及定义,但是也有一些不可以用Fluent API来设定,比如 MinimumLength
因为MinimumLength
不会对数据库结构产生影响,而只是对客户端验证起作用;
有点程序员比较喜欢用Fluent API,这样模型定义看起来“干净”很多,也可以用属性定义和Fluent API混合起来使用;
以下的Fluent API定义了多对多的连接关系所采用的表名:
using EFTest.Models; using System.Data.Entity; using System.Data.Entity.ModelConfiguration.Conventions; namespace EFTest.DAL { public class SchoolContext : DbContext { public SchoolContext() : base("SchoolContext") { } public DbSet<Student> Students { get; set; } public DbSet<Enrollment> Enrollments { get; set; } public DbSet<Course> Courses { get; set; } public DbSet<Instructor> Instructors { get; set; } public DbSet<Department> Departments { 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")); } } }
初始化种子数据
模型调整好后,在MigrationsConfiguration.cs文件中Seed方法中放入测试种子数据:
namespace EFTest.Migrations { using DAL; using Models; using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Migrations; using System.Linq; internal sealed class Configuration : DbMigrationsConfiguration<EFTest.DAL.SchoolContext> { public Configuration() { AutomaticMigrationsEnabled = false; } protected override void Seed(EFTest.DAL.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)); } } }
数据库迁移升级
在PM控制台输入: add-Migration ComplexDataModel
这个时候如果直接输入 update-database 会报错,那是因为Course这个表里已经有数据,如果直接加不可为空的列是会报错的;
所以需要在刚刚生成的带时间戳的 XXXXXXXXXXX_ComplexDataModel文件中如下面增加修改部分代码:
即先插入一条临时的Department ,然后把现在所有的Course数据行在增加Department列的时候,指向这个临时Department.
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));
最后执行 update-database 即可。