APS.NET MVC + EF (02)---ADO.NET Entity FrameWork
2.1 Entity Framework简介
Ado.net Entity Framework 是Microsoft推出的ORM框架。
2.1.1 什么是ORM
对象关系映射(Object Relational Mapping,简称ORM)模式是一种为了解决面向对象与关系数据库存在的互不匹配的现象的技术。它的作用是在关系型数据库和对象之间作一个映射,这样,我们在具体的操作数据库的时候,就不需要再去和复杂的SQL语句打交道,只要像平时操作对象一样操作它就可以了 。
ORM三个字母分别代表如下图所示:
图2-1 ORM关系图
2.1.2 Entity Framework 体系结构
Entity Framework的体系结构如图2-2所示,EMD是整个框架实现ORM最核心的部分。
图2-2 Entity Framework 体系结构
EDM(Entity Data Model) 即实体数据模型,它能够将关系数据库模型映射为实体数据模型。由以下三种模型和具有相应文件扩展名的映射文件进行定义。
- CSDL:概念层,定义概念模型,用实体来来表示数据库中的对象。(O)
- SSDL:存储层,定义存储模型,描述了表、列、关系、主键、索引等数据库中的概念。(R)
- MSL: 对应层,定义存储模型与概念模型之间的映射(M)
EF包括三种模式:DBFirst、CodeFist、ModelFirst 。EF可以调用SQL语句、可以使用Linq查询、可以使用Lambda查询,EF还有很多高级属性,比如延迟加载、缓存属性等等。
2.2 从数据库到对象模型(Database First)
2.2.1 添加实体数据模型。
在项目中添加"ADO.NET实体数据模型",选择"来自数据库的EF设计器",按照提示配置数据库相关信息。选择相关的数据库表。如图2-3、图2-4、图2-5所示。
图2-4 项目中添加"ADO.NET实体数据模型"
图2-4 选择模型内容
图2-5 选择数据库对象
完成以上步骤后,就可以在项目中看到生成的文件,如图2-6所示。
图2-6 项目结构
2.2.2 认识EDMX文件
双击打开.emdx文件,可以看到如图2-6所示的数据库模型图。改图完整的体现了数据库的表字段和表间关系。
图2-7 数据库模型图
将.emdx文件以XML的形式查看,可以看到完整的XML定义,如图2-8所示。
图2-8 .emdx的XML的形式
可以看到,edmx中包含SSDL、CSDL和C-S mapping三种类型的节点定义,分别对应EDM中的存储层、概念层、对应层。这也是EF实现ORM的关键配置文件。
2.2.3 实体文件
通过图2-7,生成的实体模型中是通过"导航属性"来描述数据库表之间关系的。对应生成的实体类代码如下:
Grade.cs文件
public virtual ICollection<Student> Student { get; set; } public virtual ICollection<Subject> Subject { get; set; } |
Student.cs文件
public virtual Grade Grade { get; set; } public virtual ICollection<Result> Result { get; set; } |
在Student实体类中,Grade属性表示一个学生对应一个年级,集合类型的Results表示一个学生对应多个成绩,并且使用virtual关键字修饰,表示可以延迟加载(后面会详细介绍)。
2.2.4 数据上下文
这个类文件的扩展名为".Context.cs",维护了受业务影响的对象列表,负责管理对象的增、删、改、查操作,以及相应的事务及并发问题。简单地说,它相当于一个对象数据库的管理者。.Context.cs类代码如示例1所示。
示例1
public partial class MySchoolEntities : DbContext { public MySchoolEntities() : base("name=MySchoolEntities") { }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { throw new UnintentionalCodeFirstException(); }
public virtual DbSet<Grade> Grade { get; set; } public virtual DbSet<Result> Result { get; set; } public virtual DbSet<Student> Student { get; set; } public virtual DbSet<Subject> Subject { get; set; } } |
从示例1可以看出,MySchoolEntities类中包含了多个对象的集合,相对于包含了多个表的数据库,MySchoolEntities继承的基类为DbContext,该类提供了以对象形式查询和使用实体数据的功能,属于Entity Framework体系结构中的Object Services部分。
2.2.5 操作数据
下面我们通过EF来对数据进行CRUD操作。
- 查询数据
示例2:
using (MySchoolEntities entities = new MySchoolEntities()) { //按名称查询年级编号 Grade grade = entities.Grade.SingleOrDefault(g => g.GradeName == "S2"); if (grade!=null) Console.WriteLine("S2年级的编号为{0}",grade.GradeId); //用where()方法查询符合条件的数据 IQueryable<Student> query = entities.Student.Where( s => s.GradeId == grade.GradeId); Console.WriteLine("学号\t姓名"); foreach (var stu in query) { Console.WriteLine("{0}\t{1}",stu.StudentNo,stu.StudentName); } } |
- 添加数据
示例3
using (MySchool1Entities entities = new MySchool1Entities()) { Student stu=new Student() { StudentNo="S10001", StudentName="王翱翔", GradeId=2, Sex="男" }; entities.Student.Add(stu); //向实体集添加数据 if (entities.SaveChanges()>0) //将添加的数据保存到数据库中 Console.WriteLine("添加数据成功!"); } |
- 修改数据
示例4
using (MySchool1Entities entities = new MySchool1Entities()) { //先查询要修改的数据 Student stu = entities.Student.FirstOrDefault(s => s.StudentName == "王翱翔"); if (stu!=null) { stu.LoginPwd = "123456"; //修改对象属性值 } if (entities.SaveChanges()>0) //将修改的对象保存到数据库中 { Console.WriteLine("修改数据成功!"); } } |
- 删除数据
示例5
using (MySchool1Entities entities = new MySchool1Entities()) { //先查询要删除的数据 Student stu = entities.Student.FirstOrDefault(s => s.StudentName == "王翱翔"); if (stu!=null) { entities.Student.Remove(stu); //从数据集中移除数据 } if (entities.SaveChanges()>0) { Console.WriteLine("删除数据成功!"); } } |
从以上代码可以看出,所有对数据库的操作,都可以通过对强类型对象的操作间接完成。需要额外做的两件事是创建数据上下文对象和调用其SaveChange()方法实现数据的增、删、改。
2.3 从代码到数据库(Code First)
在开发过程中,经常会先设计数据模型,再根据数据模型创建相应的数据库表。EF的Code First模式支持这种方式。
2.3.1 Entity Framework Code First 的配置步骤
-
安装Entity Framework package
通过VS的Nuget的包管理器添加对Entity Framework的引用。
图2-9 按照EnityFramework包
-
创建实体类
创建年级和学生两个实体类,代码如下。
public class Grade { public int GradeId { get; set; } public string GradeName { get; set; } }
public class Student { public string StudentId { get; set; } public string StudentName { get; set; } public int Age { get; set; } public int GradeId { get; set; } } |
-
创建DbContext类
首先在配置文件中配置数据库连接字符串。
<configuration> <connectionStrings> <add name="MySchool" providerName="System.Data.SqlClient" connectionString="Data Source=.;Initial Catalog=Test;Persist Security Info=True;User ID=sa;Password=sa"/> </connectionStrings> </configuration> |
注意:<connectionStrings>中必须指定"providerName"属性。
接下来创建MySchoolContext类,继承自DbContext类(命名空间:System.Data.Entity)。代码如下:
using System.Data.Entity; public class MySchoolContext:DbContext { //构造函数,会读取配置文件中的连接字符串 public MySchoolContext() :base("MySchool") //.config文件数据库连接字符串名称 { } //对象集合 public DbSet<Grade> Grade { get; set; } public DbSet<Student> Student { get; set; }
//重新父类中的方法(当模型初始化后被调用) protected override void OnModelCreating(DbModelBuilder modelBuilder) { //指定单数形式的表名(否则数据库的表名会是复数形式) //需要命名空间:using System.Data.Entity.ModelConfiguration.Conventions; modelBuilder.Conventions .Remove<PluralizingTableNameConvention>(); } } |
当程序运行后,当第一次使用EF访问数据时,数据库会被创建。可在SQL Server中查看生成的数据库及数据表。
2.4 EF Code First中的约定
通过查看生成的数据库表,发现生成的表存在很多不满足实际的地方,如主键的指定,数据长度、是否可空等等。这是由EF的默认约定来确定的。
2.4.1 EF中的默认约定
- 关于表和字段名的约定
Code First 约定表名使用英语语法的类名复数形式来命名表名,属性映射的列使用与类中属性一致的名字命名。
-
关于主键的约定
Code First默认约定将命名为 Id 或 [类名]Id 的属性视为类的键,如果是int类型同时会设为标识列。
-
字符串属性的约定
字符串约定为映射到不限长度的非空列中,默认数据类型为nvarchar(max),且允许为空。
-
布尔值的约定
Code Frirst将bool属性映射为 bit 类型,且不允许为空。
4. 关系
- 1对1关系
一个学生对应一个或零个地址,一个地址只能对应一个学生。
实现方式:
Student表中添加StudentAddress属性,StudentAddress表中添加Student属性,然后给StudentAddress中的主键加上[ForeignKey("stu")] (stu为地址表的属性)。
- 1对多关系
一个年级有多个学生,一个学生只能在一个年级。
实现方式:
Student表中添加Grade属性,Grade表中添加 ICollection<Student>属性。生成的数据库Student表中,会增加了一个新的字段grade_gradeId作为外键。
通常情况,我们会自己指定外键,方法如下:在Student类中增一个 GradeId属性,并指定为外键。
[ForeignKey("stu")]
public int GradeId { get; set; }
- 多对多关系
一个学生有多门课,一门课有多个学生上。
实现方式:
Student表中添加 ICollection<Course>属性,Course表中添加ICollection<Student>属性。生成的数据库中多了一张表:StudentCourse,里面有两个外键。
2.4.2 使用配置来覆写约定
Code First允许你覆写约定,方法是添加配置.可以选择使用Data Annotation的特性标记也可以使用强类型的Fluent API来配置。
-
使用Data Annotation特性
Data Annotations是最简单的配置方式,直接应用到你的类和类属性上。
-
System.ComponentModel.DataAnnotations命名空间下的特性是表中列的属性的。包括:Key、Required、MinLength和MaxLength、StringLength、
Timestamp、ConcurrencyCheck。
- System.ComponentModel.DataAnnotations.Schema命名空间下的特性是控制数据库结构的。包括:Table、Column、ForeignKey、NotMapped。
表2-1 EF中常用特性
特性 |
说明 |
Key |
声明主键 |
Required |
非空声明 |
MinLength/MaxLength |
设置string类型的最大长度和最小长度,数据库的对应nvarchar |
StringLength |
设置string类型的长度,数据库对应nvarchar |
Timestamp |
将byte[]类型设置为timestamp类型 |
ConcurrencyCheck |
并发检查,执行update操作时,会检查并发性(乐观锁) |
Table |
给代码中的类换一个名来映射数据库中的表名.(还可以设置表的架构名称 [Table("myAddress", Schema = "Admin")] ) |
Column |
给代码中类的属性换一个名来映射数据库中表的列名. (还可以设置列的类型、列在表中的显示顺序 [Column("myAddressName2", Order = 1, TypeName = "varchar")]) |
ForeignKey |
设置外键 |
NotMapped |
类中的列名不在数据库表中映射生成. (还可以只设置get属性或者只设置set属性,在数据库中也不映射) |
示例6
public class Student { [Key] //主键声明 public string studentKey { get; set; }
[Required] //非空声明 public string stuName { get; set; }
[MaxLength(10)] //最大长度 public string stuTxt1 { get; set; }
[MaxLength(10), MinLength(2)] //最大长度和最小长度 public string stuTxt2 { get; set; }
[ConcurrencyCheck] //并发检查 public string stuTxt3 { get; set; }
public virtual Address stuAddress { get; set; } }
[ Table("myAddress")] //设置类映射的数据库表名 public class Address { [ForeignKey("stu")] //设置外键(对应下面声明的 stu) //这里符合 类名+id(忽略大小写)的规则,所以自动生成主键 public string AddressId { get; set; }
[Column("myAddressName")] //设置映射数据库中表的列名 public string AddressName { get; set; }
[Column("myAddressName2", Order = 1, TypeName = "varchar")] //设置映射数据库中表的列名、顺序、类型 public string AddrssName2 { get; set; }
[NotMapped]//不映射数据 public string addressNum { get; set; }
public virtual Student stu { get; set; } } |
-
使用Fluent API
Fluent API 形式,可以将一个类映射成多个数据库表,还可以将配置写成多个文件,方便控制。
(1)优先级:Fluent API > data annotations > default conventions.
(2)所有的Fluent API配置都要在 OnModelCreating这个重写方法中进行
(3)常见的配置:
① 获取表对应的配置根: var stu =modelBuilder.Entity<XXX>();
② 设置主键:HasKey<string>(s => s.studentKey);
③ 获取属性:stu.Property(p => p.stuName)
④ 设置可空或非空:IsRequired和IsOptional
⑤ 设置最大值:HasMaxLength
⑥ 修改属性名→修改属性的次序→修改属性对应的数据库类型:
HasColumnName→HasColumnOrder→HasColumnType
⑦ 修改表名:ToTable
(4)可以建立多个Fluent API的配置文件,然后通过
modelBuilder.Configurations.Add(new XXX());添加到一起。
示例7
public class Student { //主键声明 public string studentKey { get; set; } //非空声明 public string stuName { get; set; } //最大长度 public string stuTxt1 { get; set; } //最大长度和最小长度 public string stuTxt2 { get; set; } //并发检查 public string stuTxt3 { get; set; } } public class Address { //既是主键、又是外键 public string AddressId { get; set; } //设置映射数据库中表的列名 public string AddressName { get; set; } //设置映射数据库中表的列名、顺序、类型 public string AddrssName2 { get; set; } //不映射数据 public string addressNum { get; set; } }
/// <summary> /// Game实体,与其它两个没有什么直接关系,单纯的为了演示, Fluent API的配置,可以根据实体进行拆分 /// 文件来配置,方便管理 /// </summary> public class Game { public int GameId { get; set; } public string GameName { get; set; } }
/// <summary> /// Game实体的配置文件 /// </summary> public class GameConfiguration : EntityTypeConfiguration<Game> { public GameConfiguration() { this.HasKey(p => p.GameId); this.Property(p => p.GameName).HasMaxLength(10).IsRequired(); } }
public class dbContext : DbContext { public dbContext() : base("name=MySchool") {
}
public DbSet<Student> Student { get; set; }
public DbSet<Address> Address { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { //所有的FluentAPI均在方法中进行重写 //一. 属性层次上的设置 var stu = modelBuilder.Entity<Student>(); var stuAddress = modelBuilder.Entity<StudentAddress>();
//1. 设置主键 stu.HasKey<string>(s => s.studentKey); stuAddress.HasKey<string>(s => s.AddressId);
//2. 设置非空 (扩展:IsOptional 设置可空) stu.Property(p => p.stuName).IsRequired( );
//3. 设置最大值(不能设置最小值) stu.Property(p => p.stuTxt1).HasMaxLength(10);
//4. 修改列的名称、排序、类型 stuAddress.Property(p => p.stuAddrssName2).HasColumnName("myAddress2").HasColumnOrder(1).HasColumnType("varchar");
//5.修改表名 stu.Map<Student>(c => { c.ToTable("MyStudent"); });
//6.将一个实体映射成多张表,并分别给其命名 //stuAddress.Map<StudentAddress>(c => //{ // c.Properties(p => new // { // p.AddressId, // p.AddressName // }); // c.ToTable("MyStuAddress1"); //}).Map<StudentAddress5>(c => //{ // c.Properties(p => new // { // p.stuAddressId, // p.stuAddrssName2 // }); // c.ToTable("MyStuAddress2"); //});
//三. 将Game实体的配置添加进来 modelBuilder.Configurations.Add(new GameConfiguration());
base.OnModelCreating(modelBuilder); } }
|
2.5 EF CodeFirst 数据库初始化策略和数据迁移
无论是DataAnnotation还是Fluent API,都会发现一个现象,当数据库结构发生变化的时候,就会抛出异常,不得不把数据库删除,重新生成,这样就会导致原数据库中的数据丢失,在实际开发中,显然是不可取的。怎么解决这个数据丢失的问题呢?
2.5.1 数据库初始化策略
EF的CodeFirst模式下数据库的初始化有四种策略来决定结构方式改变时如何处理:
- CreateDatabaseIfNotExists:EF的默认策略,数据库不存在,生成数据库;一旦model发生变化,抛异常,提示走数据迁移
- DropCreateDatabaseIfModelChanges:一旦model发生变化,删除数据库重新生成
- DropCreateDatabaseAlways:数据库每次都重新生成
- 自定义初始化(继承上面的三种策略中任何一种,然后追加自己的业务)
也可以关闭数据库初始化策略:
Database.SetInitializer<dbContext6>(null);
关闭后改变实体类,不会报错,不会丢失数据,但也无法映射改变数据库结构。
示例8
public class MySchoolEntities : DbContext {
public MySchoolEntities () : base("name=MySchool") { //在这里可以改变生成数据库的初始化策略 //1. CreateDatabaseIfNotExists (EF的默认策略,数据库不存在,生成数据库;一旦model发生变化,抛异常,提示走数据迁移) //Database.SetInitializer<MySchoolEntities>(new CreateDatabaseIfNotExists<MySchoolEntities >());
//2. DropCreateDatabaseIfModelChanges (一旦model发生变化,删除数据库重新生成) //Database.SetInitializer<MySchoolEntities >(new DropCreateDatabaseIfModelChanges<MySchoolEntities >());
//3.DropCreateDatabaseAlways (数据库每次都重新生成) //Database.SetInitializer<MySchoolEntities >(new DropCreateDatabaseAlways<MySchoolEntities >());
//4. 自定义初始化(继承上面的三种策略中任何一种,然后追加自己的业务) //Database.SetInitializer<MySchoolEntities >(new MySpecialIntializer());
//5. 禁用数据库策略(不会报错,不会丢失数据,但是改变不了数据库的结构了) //Database.SetInitializer<MySchoolEntities >(null); }
public DbSet<Animal> Animal { get; set; }
public DbSet<AnimalKind> AnimalKind { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); }
/// <summary> /// 自定义一个初始化策略,每次初始化时追加10条数据 /// </summary> public class MySpecialIntializer : DropCreateDatabaseAlways<MySchoolEntities > { public override void InitializeDatabase(MySchoolEntities context) { base.InitializeDatabase(context); }
//重写seed,追加初始数据 protected override void Seed(MySchoolEntities context) { for (int i = 0; i < 10; i++) { context.Animal.Add(new Animal() { id = "animal" + i, animalName = "mru" + i, animalKind = "mruru" + i, }); } context.SaveChanges(); base.Seed(context); } } } |
2.5.2 数据迁移
数据迁移能很好的解决修改表结构,数据丢失的问题。
方式一:自动迁移
步骤:
(1)新建Configuration.cs类,在其构造函数中进行相关配置。
(2)改变数据库的初始化策略为,MigrateDatabaseToLatestVersion ,并在Configuration类中配置开启自动迁移:AutomaticMigrationsEnabled = true;
(3)显式开启允许修改表结构:AutomaticMigrationDataLossAllowed = true; (默认是关闭的)
示例9
internal sealed class Configuration : DbMigrationsConfiguration<MySchoolEntities > { public Configuration() { AutomaticMigrationsEnabled = true; //启用自动迁移 AutomaticMigrationDataLossAllowed = true; //更改数据库中结构(增加、删除列、修改列、改变列的属性、增加、删除、修改表),需要显示开启。 } }
public class MySchoolEntities : DbContext { public MySchoolEntities () : base("name=MySchool") { //数据库迁移配置
//数据库迁移的初始化方式 Database.SetInitializer(new MigrateDatabaseToLatestVersion<MySchoolEntities , Configuration>("MySchoolEntities")); }
public DbSet<Animal> Animal { get; set; } public DbSet<AnimalKind> AnimalKind { get; set; }
protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); } }
|
进行测试:
(1)增加一列:改变Animal类,增加一个字段,刷新数据库,发现数据多了一列 (无需特别的配置),但后面再次增加,还是需要配置AutomaticMigrationDataLossAllowed设置为true
(2)删除一列、修改列、改变列的属性:报错,提示需要将AutomaticMigrationDataLossAllowed设置为true, 设置后,修改或删除成功。
(3)增加一张表:增加一个AnimalKind类,运行代码,发现数据库多了一张表。
(4) 删除表:注释掉:public DbSet<AnimalKind> AnimalKind { get; set; } ,运行代码,数据库中AnimalKind表被删除。
方式二: 手动迁移
步骤:
在程序包管理控制台中输入:
- Enable-Migrations:在项目中启用数据库迁移,然后会创建一个Configuration类
2. Add-Migration:创建了一个迁移类,其中指定了Up和Down方法。
3. Update-Database:执行Add_migration指令中创建的迁移,将改变应用到数据库中。
在使用Add-Migration命令之后,你需要更新数据库。通过执行Update-Database命令,来提交修改到数据库中,还可以在后面加上–verbose 就可以看到生成的SQL脚本
到这个时候,数据库就被创建或更新了,现在不管什么时候,模型发生改变的时候,执行Add-Migration 带上参数名,就创建一个新的迁移文件,然后执行Update-Database命令,就将修改提交到数据库了。
迁移回退
假设你想要回退到之前的任何一个状态,那么你可以执行update-database后面跟着–TargetMigration,指定你想要回退的版本。例如,假设SchoolDB数据库有很多迁移记录,但是你想回退到第一个版本,那么你可以执行下面的代码:
PM> update-database -TargetMigration:SchoolDB-v1
2.6 小结
EF Code First 配置步骤
- 安装Entity Framework Package
- 创建实体类并设置约定
- 创建DbContext类,为每个实体类创建一个DbSet
- 在配置文件中配置数据库连接字符串
- 创建Initializer类, 指定EF初始化数据库策列
- 创建Configuration 类启用数据迁移