EF6学习笔记(六) 创建复杂的数据模型

时间:2022-02-24 07:02:26

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()]
public string LastName { get; set; }
[StringLength(, 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页面,就会有以下错误:

EF6学习笔记(六) 创建复杂的数据模型

说明数据模型变了,数据库也需要先升级才可以;

先在PM控制台执行 add-migration MaxLengthOnNames

EF6学习笔记(六) 创建复杂的数据模型

会自动创建一个带时间戳的数据升级文件;然后在PM控制台继续输入:update-database

EF6学习笔记(六) 创建复杂的数据模型

完成后,就可以再次尝试进入应用的Student页面,并点击Create进入新建页面:

EF6学习笔记(六) 创建复杂的数据模型

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()]
public string LastName { get; set; }
[StringLength(, 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):

EF6学习笔记(六) 创建复杂的数据模型

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()]
[Display(Name = "Last Name")]
public string LastName { get; set; }
[Required]
[StringLength(, 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(, MinimumLength=)]
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()]
public string LastName { get; set; } [Required]
[Column("FirstName")]
[Display(Name = "First Name")]
[StringLength()]
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(, MinimumLength = )]
public string LastName { get; set; } [Column("FirstName"), Display(Name = "First Name"), StringLength(, MinimumLength = )]
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()]
[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(, MinimumLength = )]
public string Title { get; set; } [Range(, )]
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(, MinimumLength = )]
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; }
} }

最后希望的模型定义图:

EF6学习笔记(六) 创建复杂的数据模型

做些总结汇总:

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

初始化种子数据

模型调整好后,在Migrations\Configuration.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 = ,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Abercrombie").ID },
new Department { Name = "Mathematics", Budget = ,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Fakhouri").ID },
new Department { Name = "Engineering", Budget = ,
StartDate = DateTime.Parse("2007-09-01"),
InstructorID = instructors.Single( i => i.LastName == "Harui").ID },
new Department { Name = "Economics", Budget = ,
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 = , Title = "Chemistry", Credits = ,
DepartmentID = departments.Single( s => s.Name == "Engineering").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Microeconomics", Credits = ,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Macroeconomics", Credits = ,
DepartmentID = departments.Single( s => s.Name == "Economics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Calculus", Credits = ,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Trigonometry", Credits = ,
DepartmentID = departments.Single( s => s.Name == "Mathematics").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Composition", Credits = ,
DepartmentID = departments.Single( s => s.Name == "English").DepartmentID,
Instructors = new List<Instructor>()
},
new Course {CourseID = , Title = "Literature", Credits = ,
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: ));

最后执行 update-database 即可。