Entity Framework Core
Entity Framework Core
Entity Framework Core 是.NetCore 中的 ORM(Object Relational Mapping,对象关系映射)框架,让开发者用对象操作的形式操作关系数据库。ORM 只是对 ADO.NET 的封装,ORM 底层仍然是通过 ADO.NET 访问数据库的。
EF Core 与其他 ORM 比较:
1、Entity Framework Core(EF Core)是微软官方的 ORM 框架。优点:功能强大、官方支持、生产效率高、力求屏蔽底层数据库差异;缺点:复杂、上手门槛高、不熟悉 EFCore 的话可能会进坑。
2、Dapper。优点:简单,N 分钟即可上手,行为可预期性强;缺点:生产效率低,需要处理底层数据库差异。
3、EF Core 是模型驱动(Model-Driven)的开发思想,Dapper 是数据库驱动(DataBase-Driven)的开发思想的。没有优劣,只有比较。
4、性能: Dapper 等 ≠ 性能高;EF Core≠ 性能差。
5、EF Core 是官方推荐、推进的框架,尽量屏蔽底层数据库差异,.NET 开发者必须熟悉,根据的项目情况再决定用哪个。
个人选择:
1、对于后台系统、信息系统等和数据库相关开发工作量大的系统且团队比较稳定,用 EF Core;对于互联网系统等数据库相关工作量不大的系统,或者团队不稳定,用 Dapper。
2、在项目中可以混用,只要注意 EF Core 的缓存、Tracking 等问题即可。
EF Core 与 EF 比较:
1、EF 有 DB First、Model First、Code First。EF Core 不支持模型优先,推荐使用代码优先,遗留系统可以使用 .Scaffold-DbContext 来生成代码实现类似 DBFirst 的效果,但是推荐用 Code First 。
2、EF 会对实体上的标注做校验,EF Core 追求轻量化,不校验。
3、熟悉 EF 的话,掌握 EFCore 会很容易,很多用法都移植过来了。EF Core 又增加了很多新东西。
4、EF 中的一些类的命名空间以及一些方法的名字在 EF Core 中稍有不同。
5、EF 不再做新特性增加。
EFCore 实体的配置
主要规则:
1:表名采用 DbContext 中的对应的 DbSet 的属性名。
2:数据表列的名字采用实体类属性的名字,列的数据类型采用和实体类属性类型最兼容的类型。
3:数据表列的可空性取决于对应实体类属性的可空性。
4:名字为 Id 的属性为主键,如果主键为 short, int 或者 long 类型,则默认采用自增字段,如果主键为 Guid 类型,则默认采用默认的 Guid 生成机制生成主键值。
两种配置方式
1、Data Annotation
把配置以特性(Annotation)的形式标注在实体类中。
[Table(“T_Books”)]
public class Book
{
}
优点:简单;缺点:耦合。
2、Fluent API
builder.ToTable(“T_Books”);
把配置写到单独的配置类中。
缺点:复杂;优点:解耦
3、大部分功能重叠。可以混用,但是不建议混用。
FluentAPI
1、视图与实体类映射:modelBuilder.Entity
2、排除属性映射:
modelBuilder.Entity
3、配置列名:
modelBuilder.Entity
4、配置列数据类型:
builder.Property(e => e.Title) .HasColumnType(“varchar(200)”)
5、配置主键
默认把名字为 Id 或者“实体类型+Id“的属性作为主键,可以用 HasKey()来配置其他属性作为主键。
modelBuilder.Entity
支持复合主键,但是不建议使用。
6、生成列的值
modelBuilder.Entity
7、可以用 HasDefaultValue()为属性设定默认值
modelBuilder.Entity
8、索引
modelBuilder.Entity
复合索引
modelBuilder.Entity
唯一索引:IsUnique();聚集索引:IsClustered()
9、用 EF Core 太多高级特性的时候谨慎,尽量不要和业务逻辑混合在一起,以免“不能自拔”。比如 Ignore、Shadow、Table Splitting 等
10、Fluent API 中很多方法都有多个重载方法。比如 HasIndex、Property()。
把 Number 属性定义为索引,下面两种方法都可以:
builder.HasIndex(“Number”);
builder.HasIndex(b=>b.Number);
推荐使用 HasIndex(b=>b.Number)、Property(b => b.Number)这样的写法,因为这样利用的是 C#的强类型检查机制
总结:
1、 Data Annotation 、Fluent API 大部分功能重叠。可以混用,但是不建议混用。
2、有人建议混用,即用了 Data Annotation 的简单,又用到 Fluent API 的强大,而且实体类上标注的[MaxLength(50)]、[Required]等标注可以被 ASP.NET Core 中的验证框架等复用。我为什么不建议混用。
3、我和业界很多人都倾向只使用 Fluent API。本课以讲解 Fluent API 为主(尽量用约定),如果项目强制用 Data Annotation 请翻文档,知识都是通用的。
主键
自增主键
1、 EF Core 支持多种主键生成策略:自动增长;Guid;Hi/Lo 算法等。
2、自动增长。优点:简单;缺点:数据库迁移以及分布式系统中比较麻烦;并发性能差。long、int 等类型主键,默认是自增。因为是数据库生成的值,所以 SaveChanges 后会自动把主键的值更新到 Id 属性。试验一下。场景:插入帖子后,自动重定向帖子地址。
3、自增字段的代码中不能为 Id 赋值,必须保持默认值 0,否则运行的时候就会报错。
Guid 主键
1、 Guid 算法(或 UUID 算法)生成一个全局唯一的 Id。适合于分布式系统,在进行多数据库数据合并的时候很简单。优点:简单,高并发,全局唯一;缺点:磁盘空间占用大。
2、Guid 值不连续。使用 Guid 类型做主键的时候,不能把主键设置为聚集索引。因为聚集索引是按照顺序保存主键的,因此用 Guid 做主键性能差。比如 MySQL 的 InnoDB 引擎中主键是强制使用聚集索引的。有的数据库支持部分的连续 Guid,比如 SQLServer 中的 NewSequentialId(),但也不能解决问题。在 SQLServer 等中,不要把 Guid 主键设置为聚集索引;在 MySQL 中,插入频繁的表不要用 Guid 做主键。
其他方案
1、 混合自增和 Guid(非复合主键)。用自增列做物理的主键,而用 Guid 列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据时候把 Guid 当主键用。在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用 Guid 列。不仅保证了性能,而且利用了 Guid 的优点,而且减轻了主键自增性导致主键值可被预测带来的安全性问题。
2、Hi/Lo 算法:EF Core 支持 Hi/Lo 算法来优化自增列。主键值由两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或者集群中不同服务器获取的 Hi 值不会重复,而本地进程计算的 Lo 则可以保证可以在本地高效率的生成主键值。但是 HiLo 算法不是 EF Core 的标准。
Migrations
Add-Migration
1、使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移”(Up),也可以执行把数据库回退到旧的迁移,这个操作叫“向下迁移”(Down)。
2、除非有特殊需要,否则不要删除 Migrations 文件夹下的代码。
3、进一步分析 Migrations 下的代码。分析 Up、Down 等方法。查看 Migration 编号。
4、查看数据库的__EFMigrationsHistory 表:记录当前数据库曾经应用过的迁移脚本,按顺序排列。
Migrations 其他命令
1、Update-Database XXX
把数据库回滚到 XXX 的状态,迁移脚本不动。
2、Remove-migration
删除最后一次的迁移脚本
3、Script-Migration
生成迁移 SQL 代码。有了 Update-Database 为什么还要生成 SQL 脚本。
可以生成版本 D 到版本 F 的 SQL 脚本:
Script-Migration D F
生成版本 D 到最新版本的 SQL 脚本:Script-Migration D
反向工程
1、根据数据库表来反向生成实体类
2、Scaffold-DbContext ‘Server=.;Database=demo1;Trusted_Connection=True;MultipleActiveResultSets=true’ Microsoft.EntityFrameworkCore.SqlServer
注意
1、生成的实体类可能不能满足项目的要求,可能需要手工修改或者增加配置。
2、再次运行反向工程工具,对文件所做的任何更改都将丢失。
3、不建议把反向工具当成了日常开发工具使用,不建议 DBFirst。
EFCore 底层操作数据库
框架是帮助程序员简化工作的,不是把程序员当做傻瓜!!!
应用程序通过 ORM 转化为 ADO.NET CORE 操作数据库。
EFCore 把 C#代码转换为 SQL 语句的框架,查看生成的 SQL 语句:
1、SQL Server Profiler 查看 SQLServer 数据库当前执行的 SQL 语句。
2、var books = ctx.Books.Where(b => b.Price > 10 || b.Title.Contains(“张”));
EFCore 做不到的事
1、C#千变万化;SQL 功能简单。存在合法的 C#语句无法被翻译为 SQL 语句的情况:
var books = ctx.Books.Where(b => IsOK(b.Title)); |
2、不同数据库的不同
AST:抽象语法树
不同数据库是否有对应的 EFCoreProvider,以及 provider 的版本须一致
通过代码查看 EF Core 生成的 sql
方法 1:标准日志
public static readonly ILoggerFactory MyLoggerFactory |
方法 2:简单日志
optionsBuilder.LogTo(Console.WriteLine); |
方法 3:ToQueryString
1、上面两种方式无法直接得到一个操作的 SQL 语句,而且在操作很多的情况下,容易混乱。
2、EF Core 的 Where 方法返回的是 IQueryable 类型,DbSet 也实现了 IQueryable 接口。 IQueryable 有扩展方法 ToQueryString()可以获得 SQL。
3、不需要真的执行查询才获取 SQL 语句;只能获取查询操作的。
总结
写测试性代码,用简单日志;正式需要记录 SQL 给审核人员或者排查故障,用标准日志;开发阶段,从繁杂的查询操作中立即看到 SQL,用 ToQueryString()。
同样的 LINQ 被翻译为不同的 SQL 语句
不同数据库方言不同
SQLServer: select top(3) _ from t
MySQL: select _ from t LIMIT 3
Oracle: select * from t where ROWNUM<=3
同样的 C#语句在不同数据库中被 EF Core 翻译成不同的 SQL 语句。
EF Core 迁移脚本和数据库相关
因此迁移脚本不能跨数据库。通过给 Add-Migration 命令添加“-OutputDir”参数的形式来在同一个项目中为不同的数据库生成不同的迁移脚本。
mysql 被 oracle 收购,社区版和商业版;postgresql 免费开源
MySQL 项目中测试
1、EF Provider 的选择 Install-Package Pomelo.EntityFrameworkCore.MySql
2、optionsBuilder.UseMySql(“server=localhost;user=root;password=root;database=ef”,
new MySqlServerVersion(new Version(5, 6, 0)));
PostgreSQL 项目中测试
Install-Package Npgsql.EntityFrameworkCore.PostgreSQL
optionsBuilder.UseNpgsql(“Host=127.0.0.1;Database=ef;Username=postgres;Password=123456”);
EFCore 实体间关系
什么是实体间关系
1、所谓“关系数据库”
2、数据库表之间的关系:一对一、一对多、多对多。
3、EF Core 不仅支持单实体操作,更支持多实体的关系操作。
4、实体类中关系属性;FluentAPI 关系配置;使用关系操作。
一对多:实体类
1、文章实体类 Article、评论实体类 Comment。一篇文章对应多条评论。
public class Article |
一对多:关系配置
EF Core 中实体之间关系的配置的套路:
HasXXX(…).WithXXX(…);
有 XXX、反之带有 XXX。
XXX 可选值 One、Many。
一对多:HasOne(…).WithMany(…);
一对一:HasOne(…).WithOne (…);
多对多:HasMany (…).WithMany(…);
class ArticleConfig : IEntityTypeConfiguration<Article> |
一对多关系数据的获取
获取关系数据:
Article a = ctx.Articles.Include(a=>a.Comments).Single(a=>a.Id==1); |
Include 定义在 Microsoft.EntityFrameworkCore 命名空间中。查看一下生成的 SQL 语句
额外的外键字段
为什么需要外键属性
1、EF Core 会在数据表中建外键列。
2、如果需要获取外键列的值,就需要做关联查询,效率低。试一下。
3、需要一种不需要 Join 直接获取外键列的值的方式。
设置外键属性
1、在实体类中显式声明一个外键属性。
2、关系配置中通过 HasForeignKey(c=>c.ArticleId)指定这个属性为外键。
3、除非必要,否则不用声明,因为会引入重复。

EFCore 大部分查询比绝大多数程序员写出来的 sql 性能都要高!!!
单向导航属性
EF Core 悲观并发控制
概念
1、并发控制:避免多个用户同时操作资源造成的并发冲突问题。举例:统计点击量。
2、最好的解决方案:非数据库解决方案。
3、数据库层面的两种策略:悲观、乐观。
4、 悲观并发控制一般采用行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。EF Core 没有封装悲观并发控制的使用,需要开发人员编写原生 SQL 语句来使用悲观并发控制。不同数据库的语法不一样。
实现
class House { |
MySQL 方案: select * from T_Houses where Id=1 for update
如果有其他的查询操作也使用 for update 来查询 Id=1 的这条数据的话,那些查询就会被挂起,一直到针对这条数据的更新操作完成从而释放这个行锁,代码才会继续执行。
1、锁是和事务相关的,因此通过 BeginTransactionAsync()创建一个事务,并且在所有操作完成后调用 CommitAsync()提交事务。
2、var h1 = await ctx.Houses.FromSqlInterpolated($”select * from T_Houses where Id=1 for update”).SingleAsync();
3、定位到编译完成的 exe 目录下,运行两个 exe 程序的实例,分别输入姓名 tom 和 jim。
Console.WriteLine("请输入您的姓名"); |
结果

问题
1、悲观并发控制的使用比较简单;
2、锁是独占、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当的话,甚至会导致死锁。
3、不同数据库的语法不一样。
乐观并发控制:并发令牌
原理
Update T_Houses set Owner=新值
where Id=1 and Owner=旧值
举例子。当 Update 的时候,如果数据库中的 Owner 值已经被其他操作者更新为其他值了,那么 where 语句的值就会为 false,因此这个 Update 语句影响的行数就是 0,EF Core 就知道“发生并发冲突”了,因此 SaveChanges()方法就会抛出 DbUpdateConcurrencyException 异常。
EFCore 配置
1、把被并发修改的属性使用 IsConcurrencyToken()设置为并发令牌。
2、builder.Property(h => h.Owner).IsConcurrencyToken();
3、catch(DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = await entry.GetDatabaseValuesAsync();
string newOwner = dbValues.GetValue
Console.WriteLine($”并发冲突,被{newOwner}提前抢走了”);
}
class Program |
乐观并发控制:RowVersion
1、SQLServer 数据库可以用一个 byte[]类型的属性做并发令牌属性,然后使用 IsRowVersion()把这个属性设置为 RowVersion 类型,这样这个属性对应的数据库列就会被设置为 ROWVERSION 类型。对于 ROWVERSION 类型的列,在每次插入或更新行时,数据库会自动为这一行的 ROWVERSION 类型的列其生成新值。
2、在 SQLServer 中,timestamp 和 rowversion 是同一种类型的不同别名而已。
class House |

1、在 MySQL 等数据库中虽然也有类似的 timestamp 类型,但是由于 timestamp 类型的精度不够,并不适合在高并发的系统。
2、非 SQLServer 中,可以将并发令牌列的值更新为 Guid 的值。
3、修改其他属性值的同时,使用 h1.RowVer = Guid.NewGuid()手动更新并发令牌属性的值。
总结
1、乐观并发控制能够避免悲观锁带来的性能、死锁等问题,因此推荐使用乐观并发控制而不是悲观锁。
2、如果有一个确定的字段要被进行并发控制,那么使用 IsConcurrencyToken()把这个字段设置为并发令牌即可;
3、如果无法确定一个唯一的并发令牌列,那么就可以引入一个额外的属性设置为并发令牌,并且在每次更新数据的时候,手动更新这一列的值。如果用的是 SQLServer 数据库,那么也可以采用 RowVersion 列,这样就不用开发者手动来在每次更新数据的时候,手动更新并发令牌的值了。








