EF Core实战进阶版
常用数据库 ORM 库
SQLServer Microsoft.EntityFrameworkCore.SqlServer
MySQL MySql.EntityFrameworkCore
开源代替库(推荐使用) Pomelo.EntityFrameworkCore.MySql
PostgreSQL Npgsql.EntityFrameworkCore.PostgreSQL
ORM
ORM:Object Relational Mapping。让开发者用对象操作的形式操作关系数据库。
// 插入:
User user = new User(){Name="admin", Password="123"};
orm.Save(user);
// 查询:
Book b = orm.Books.Single(b=>b.Id==3 || b.Name.Contains(".NET"));
string bookName = b.Name;
string aName = b.Author.Name;
有哪些ORM:EF Core、Dapper、SqlSugar、FreeSql 等
EF Core 与其他 ORM 比较(EF Core、Dapper)
- Entity Framework Core(EF Core)是 Microsoft 官方的ORM框架。优点:功能强大、官方支持、生产效率高、力求屏蔽底层数据库差异;缺点:复杂、上手门槛高、不熟悉EF Core的话可能会掉坑。
- Dapper 优点:简单,N分钟即可上手,行为可预期性强;缺点:生产效率低,需要处理底层数据库差异。
- EF Core是模型驱(Model-Driven)动的开发思想,Dapper是数据库驱动(DataBase-Driven)的开发思想。没有优劣,只有合适。
- 性能:Dapper等≠性能高;EFCore≠性能差。
- EF Core 是官方推荐、推进的框架,尽量屏蔽底层数据库差异,
.NET
开发者必须熟悉,根据项目情况再决定使用哪个。
EF Core 与 EF 比较
- EF有DB First、Model First、Code First。EF Core 不支持模型优先,推荐使用代码优先,遗留系统可以使用Scaffold-DBContext来生成代码实现类似DB First的效果,但是推荐使用Code First
- EF会对实体上的标注做校验,EF Core追求轻量化,不校验。
- 熟悉EF的话,掌握EF Core会很容易,很多用法都移植过来了。EF Core又增加了很多新东西。
- EF中的一些类的命名空间以及一些方法的名字在EF Core中稍有不同。
- EF不再做新特性增加。
数据库的使用
- EF Core是对于底层ADO.NET Core的封装,因此ADO.NET Core支持的数据库不一定被EF Core支持。
- EF Core支持所有主流的数据库,包括MS SQL Server、Oracle、MySQL、PostgreSQL、SQLite等。可以自己实现Provider支持其他数据库。国产数据库支持问题。
- 对于SQLServer自持最完美,MySQL、PostgreSQL也不错(有能解决的小坑)。这三者是.NET圈中用的最多的三个。
Migration 数据库迁移
面向对象的ORM开发中,数据库不是程序员手动创建的,而是由 Migration工具生成的。关系数据库只是盛放模型数据的一个媒介而已,理想状态下,程序员不用关心数据库的操作。
根据对象的定义变化,自动更新数据库中的表以及表结构的操作,叫做Migration(迁移)。
迁移可分为多步(项目进化),也可以回滚。
-
执行
Install-Package Microsoft.EntityFrameworkCore.Tools
命令确定 Visual Studio 已安装 EntityFrameworkCoreTools 工具 -
在“程序包管理控制台”中执行如下命令
Add-Migration InitialCreate
会自动在项目的Migrations文件夹中生成操作数据库的C#代码 -
代码需要执行后才会应用对数据库的操作。“程序包管理控制台”中执行
Update-Database
Fluent API
- Data Annotation、Fluent API大部分功能重叠。可以混用,但不建议混用
- 有人建议混用,即用了Data Annotation的简单,又用到Fluent API的强大,而且实体类上的标注
[MaxLength(50)]、[Required]
等标注可以被ASP.NET Core中的验证框架等复用
主键无小事
自增主键
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做主键。
3、Guid用法:既可以让EF Core给赋值,也可以手动赋值(推荐)
其他方案
1、混合自增和Guid(非复合主键)。用自增列做物理的主键,而用Guid列做逻辑上的主键。把自增列设置为表的主键,而在业务上查询数据时候把Guid当主键用,在和其他表关联以及和外部系统通讯的时候(比如前端显示数据的标识的时候)都是使用Guid列。不仅保证了性能,而且利用了Guid的优点,而且减轻了主键自增性导致主键值可被预测带来的安全性问题。
2、Hi/Lo算法:EF Core支持Hi/Lo算法来优化自增列。主键值由两部分组成:高位(Hi)和低位(Lo),高位由数据库生成,两个高位之间间隔若干个值,由程序在本地生成低位,低位的值在本地自增生成。不同进程或者集群中不同服务器获取的Hi值不会重复,而本地进程计算的Lo则可以保证可以在本地高效的生成主键值。但是Hi/Lo算法不是EF Core的标准。
深入研究Migration
1、使用迁移脚本,可以对当前连接的数据库执行编号更高的迁移,这个操作叫做“向上迁移”(Up),也可以执行把数据库回退到旧的迁移,这个操作叫做“向下迁移”(Down)。
2、除非有特殊需要,否则不要删除Migration文件夹下的代码。
Migration其他命令
Updata-Database XXX
把数据库回滚到XXX状态,迁移脚本不动
Remove-migration
删除最后一次的迁移脚本
Script-Migration M N
生成迁移SQL脚本,生成版本M到版本N到SQL脚本
反向工程
根据数据库表来反向生成实体类
Scaffold-DbContext "连接字符串" Microsoft.EntityFrameworkCore.SqlServer(数据库类型)
注意
1、生成的实体类可能不能满足项目的要求,可能需要手工修改或者增加配置。
2、再次运行反向工程工具,对文件所做的任何更改都将丢失。
3、不建议把反向工具当成日常开发工具使用,不建议DBFirst
EF Core原理
通过代码查看EF Core的SQL语句
标准日志
需要引入Logging框架 optionsBuilder.UseLoggerFactory(MyLoggerFactory)
简单日志
日志内容为EF Core运行过程,比较繁杂
optionsBuilder.LogTo(msg=>{
Console.WriteLine(msg);
});
ToQueryString
1、上面两种方式无法直接得到一个操作的SQL语句,而且在操作很多的情况下,容易混乱。
2、EF Core的Where方法返回的是IQueryable类型,DbSet也实现了IQueryable接口。IQueryable有扩展方法ToQueryString()可以获得SQL。
3、不需要真的执行查询才获取SQL语句;只能获取查询操作。
写测试代码,用简单日志;正是需要记录SQL给审核人员或者排查故障,用标准日志;开发阶段,从繁杂的查询操作中立即看到SQL,用ToQueryString()。
重点
EF Core 一对多关系
在 EF Core 中设计关系(三步):
1、实体类中关系属性;
2、FluentAPI关系配置;
3、使用关系操作。
一对多关系:(一篇文章对应多条评论)
// 文章类
public class Article
{
public long Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string Message { get; set; }
public List<Comment> Comments { get; set; } = new List<Comment>(); // 建议给一个空的List
}
// 评论
public class Comment
{
public long Id { get; set; }
public Article TheArticle { get; set; }
public string Message { get; set; }
}
// 一端配置
class ArticleConfig:IEntityTypeConfiguration<Article>
{
public void Configure(EntityTypeBuilder<Article> builder)
{
builder.ToTable("T_Article");
builder.Property(a => a.content).IsRequired().IsUnicode();
builder.Property(a => a.Title).IsRequired().IsUnicode().HasMaxLength(255);
}
}
// 多端配置
class CommentConfig:IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comment");
builder.HasOne<Article>(c => c.TheArticle).WithMany(a => a.Comments).IsRequired();
builder.Property(a => a.Message).IsRequired().IsUnicode();
}
}
EF Core中实体之间的配置套路:
HasXXX(...).WithXXX(...);
有XXX,反之带有XXX。
XXX 可选值One
、Many
一对多:HasOne(...).WithMany(...);
一对一:HasOne(...).WithOne(...);
多对多:HasMany(...).WithMany(...);
插入数据
using(MyDbContext ctx = new MyDbContext())
{
Article article = new()
{
Title = "123",
Message = "456"
};
Comment comment1 = new()
{
Message = "789"
};
Comment comment2 = new()
{
Message = "234"
};
article.Comments.Add(comment1);
article.Comments.Add(comment2);
ctx.Article.Add(article);
ctx.SaveChanges();
}
查询关系数据
使用
Include()
方法查询关联的数据
using Microsoft.EntityFrameworkCore;
using System.Linq;
using(MyDbContext ctx = new MyDbContext())
{
Article article = ctx.Article.Include(e => e.Comments).Single(e => e.Id == 1);
Console.WriteLine(article.Id);
Console.WriteLine(article.Title);
foreach(Comment cmt in article.Comments)
{
Console.WriteLIne(cmt.Message);
}
}
额外的外键字段
为什么需要外键属性:
1、EF Core会在数据表中建外键列。
2、如果需要获取外键列的值,就需要做关联查询,效率低。
3、需要一种不需要Join直接获取外键列的值的方式。
设置外键属性
1、在实体类中显式声明一个外键属性。
2、关系配置中通过HasForeignKey(c => c.TheArticleId)
指定这个属性为外键。
3、非必要,不声明,因为会重复引入。
// 评论
public class Comment
{
public long Id { get; set; }
public Article TheArticle { get; set; }
public string Message { get; set; }
public long TheArticleId { get; set; } // 新增 TheArticleId 属性
}
// 多端配置
class CommentConfig:IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comment");
// 修改代码 添加 .HasForeignKey(c => c.TheArticleId)
builder.HasOne<Article>(c => c.TheArticle).WithMany(a => a.Comments).HasForeignKey(c => c.TheArticleId).IsRequired();
builder.Property(a => a.Message).IsRequired().IsUnicode();
}
}
// 查询语句
using(MyDbContext ctx = new MyDbContext())
{
var cmt = ctx.Comment.Select(c => new{ c.Id,C.ThenArticleId }).Single( c => c.Id == 1);
}
单向导航属性
配置方法
不设置反向的属性,然后配置的时候WithMany()
不设置参数即可。
代码实现案例:
class CommentConfig:IEntityTypeConfiguration<Comment>
{
public void Configure(EntityTypeBuilder<Comment> builder)
{
builder.ToTable("T_Comment");
builder.HasOne<Article>(c => c.TheArticle).WithMany();
}
}
建议:一般关系配置在多的一端
自引用的组织结构树
代码示例
// 实体
class OrgUnit
{
public long Id { get; set; }
public string Name { get; set; }
public OrgUnit Parent { get; set; }
public List<OrgUnit> Children { get; set; } = new List<OrgUnit>();
}
// 关系配置
class OrgUnitConfig:IEntityTypeConfiguration<OrgUnit>
{
public void Configure(EntityTypeBuilder<OrgUnit> builder)
{
builder.ToTable("T_OrgUnit");
builder.Property(o => o.Name).IsUnicode().IsRequired().HasMaxLength(50);
builder.HasOne<OrgUnit>(o => o.Parent).WithMany(o => o.Children); // 根节点没有 Parent 因此这个关系不能修饰为 “不可空”
}
}
// 两种不同的插入数据的方式
// 插入数据1
OrgUnit ouRoot = new() { Name = "中天集团全球总部" };
OrgUnit ouAsia = new() { Name = "中天集团亚太区总部" };
ouAsia.Parent = ouRoot;
ouRoot.Children.Add(ouAsia);
OrgUnit ouAmerica = new() { Name = "中天集团美洲总部" };
ouAmerica.Parent = ouRoot;
ouRoot.Children.Add(ouAsiaAmerica);
OrgUnit ouUSA = new() { Name = "中天美国" };
ouUSA.Parent = ouAmerica;
ouAmerica.Children.Add(ouUSA);
OrgUnit ouCan = new() { Name = "中天加拿大" };
ouCan.Parent = ouAmerica;
ouAmerica.Children.Add(ouCan);
OrgUnit ouChina = new() { Name = "中天集团(中国)" };
ouChina.Parent = ouAsia;
ouAsia.Children.Add(ouChina);
OrgUnit ouSg = new() { Name = "中天新加坡" };
ouSg.Parent = ouAsia;
ouAsia.Children.Add(ouSg);
using(MyDbContext ctx = new())
{
ctx.OrgUnit.Add(ouRoot);
await ctx.SaveChangesAsync();
}
// 插入数据2
OrgUnit ouRoot = new() { Name = "中天集团全球总部" };
OrgUnit ouAsia = new() { Name = "中天集团亚太区总部" };
ouAsia.Parent = ouRoot;
OrgUnit ouAmerica = new() { Name = "中天集团美洲总部" };
ouAmerica.Parent = ouRoot;
OrgUnit ouUSA = new() { Name = "中天美国" };
ouUSA.Parent = ouAmerica;
OrgUnit ouCan = new() { Name = "中天加拿大" };
ouCan.Parent = ouAmerica;
ouAmerica.Children.Add(ouCan);
OrgUnit ouChina = new() { Name = "中天集团(中国)" };
ouChina.Parent = ouAsia;
OrgUnit ouSg = new() { Name = "中天新加坡" };
ouSg.Parent = ouAsia;
using(MyDbContext ctx = new())
{
ctx.OrgUnit.Add(ouRoot);
ctx.OrgUnit.Add(ouAsia);
ctx.OrgUnit.Add(ouAmerica);
ctx.OrgUnit.Add(ouUSA);
ctx.OrgUnit.Add(ouCan);
ctx.OrgUnit.Add(ouChina);
ctx.OrgUnit.Add(ouSg);
await ctx.SaveChangesAsync();
}
// 查询展示数据
using(MyDbContext ctx = new())
{
var ouRoot = ctx.OrgUnit.Single( o => o.Parent == null); // 查询根节点
var root = ouRoot.Name;
PrintChildren(1, ctx, ouRoot);
}
// 递归法
void PrintChildren(int identLevel, MyDbContext ctx, OrgUnit parent)
{
var children = ctx.OrgUnit.Where(o => o.Parent == parent);
foreach(var child in children)
{
// 以我为父节点的子节点
PrintChildren(identLevel++, ctx, child);
}
}
EF Core 一对一关系
必须显式的在其中一个实体类中声明一个外键属性
代码示例
// 实体
class Order
{
public long Id { get; set; }
public long Name { get; set; }
public long Address { get; set; }
public Delivery Delivery { get; set; }
}
class Delivery
{
public long Id { get; set; }
public string CompanyName { get; set; }
public string Number { get; set; }
public Order Order { get; set; }
public long OrderId { get; set; }
}
// 关系配置
class DeliveryConfig:IEntityTypeConfiguration<Delivery>
{
public void Configure(EntityTypeBuilder<Delivery> builder)
{
builder.ToTable("T_Deliveries");
}
}
class OrderyConfig:IEntityTypeConfiguration<Order>
{
public void Configure(EntityTypeBuilder<Order> builder)
{
builder.ToTable("T_Order");
builder.HasOne<Delivery>(o => o.Delivery).WithOne(d => d.Order).HasForeignKey<Delivery>(o => o.OrderId);
}
}
// 数据插入
uisng(MyDbContext ctx = new MyDbContext())
{
Order order = new Order();
order.Name = "书籍";
Delivery delivery = new Delivery();
delivery.CompanyName = "xx快递";
delivery.Number = "xx2022050100001";
delivery.Order = order;
// 第一种保存数据方式
ctx.Delivery.Add(delivery);
// 第二种保存数据方式
ctx.Order.Add(order);
ctx.Delivery.Add(delivery);
// 写入数据库
ctx.SaveChanges();
}
EF Core 多对多关系
EF Core 5.0开始,才正式支持多对多
需要中间表,列举数据
代码示例
// 实体
class Student
{
public long Id { get; set; }
public string Name { get; set; }
public List<Teacher> Teachers { get; set; } = new List<Teacher>();
}
class Teacher
{
public long Id { get; set; }
public string Name{ get; set; }
public List<Student> Students { get; set; } = new List<Student>();
}
// 关系配置 可以配置在任意一方,这里配置在 Student 以作参考
class StudentConfig:IEntityTypeConfiguration<Student>
{
public void Configure(EntityTypeBuilder<Student> builder)
{
builder.ToTable("T_Student");
builder.HasMany<Teacher>(s => s.Teachers).WithMany(t => t.Students)UsingEntity(j => j.ToTable("T_Students_Teachers"));
}
}
class TeacherConfig:IEntityTypeConfiguration<Teacher>
{
public void Configure(EntityTypeBuilder<Teacher> builder)
{
builder.ToTable("T_Teacher");
}
}
// 数据插入、数据读取
uisng(MyDbContext ctx = new MyDbContext())
{
// 插入
Student s1 = new Student{ Name = "张三" };
Student s2 = new Student{ Name = "李四" };
Teacher t1 = new Teacher{ Name = "Tom" };
Teacher t2 = new Teacher{ Name = "Jerry" };
s1.Teachers.Add(t1);
s1.Teachers.Add(t2);
s2.Teachers.Add(t1);
s2.Teachers.Add(t2);
ctx.Teachers.Add(t1);
ctx.Teachers.Add(t2);
ctx.Students.Add(s1);
ctx.Students.Add(s2);
ctx.SaveChanges();
// 读取
var teacgers = ctx.Teachers.Include(t => t.Students);
foreach(var t in teacgers)
{
Console.WriteLine(t.Name);
foreach(s in t.Students)
{
Console.WriteLine($"\ts.Name");
}
}
}
EF Core基于关系的复杂查询
代码示例 基于 一对多 “文章和评论实体”
// 写法一
var data = ctx.Articles.Where(a => a.Comments.Any(c => c.Message.Contains("微软")));
// 写法二
var data = ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.TheArticles);
// 或
// 去重
var data = ctx.Comments.Where(c => c.Message.Contains("微软")).Select(c => c.TheArticles).Distinct();
EF Core有了IEnumerable 还要 IQueryable干什么
不同的 Where 方法
1、对普通集合和DbSet调用的Where方法,虽然用起来一样,但是 “转到定义” 后看到的是不同的方法。
2、普通集合的版本 (IEnumerable
) 是在内存中过滤(客户端评估)而IQueryable
版本则是把查询操作翻译成SQL语句(服务器端评估)。
EF Core 所谓 IQueryable
1、IQueryable 只是代表一个 “可以放到数据库服务器去执行的查询” ,它没有立即执行,只是 “可以被执行” 而已。
2、对于 IQueryable 接口调用非终结方法的时候不会执行查询,而调用中介方法的时候则会立即执行查询。
3、终结方法:遍历、ToArray()、ToList()、Min()、Max()、Count()等;
4、非终结方法:GroupBy()、OrderBy()、Include()、Skip()、Take()等。
5、简单判断:一个方法的返回值类型如果是 IQueryable 类型,那么这个方法一般就是非终结方法,否则就是终结方法。
为什么要延迟执行
可以在实际执行之前,分布构建IQueryable。
结论:
1、IQueryable代表一个对数据库中数据进行查询的一个逻辑,这个查询是一个延迟查询。我们可以调用非终结方法向IQueryable中添加查询逻辑,当执行终结方法的时候才真正生成SQL语句来执行查询。
2、可以实现以前要靠SQL拼接实现的动态查询逻辑。
IQueryable的复用
IQueryable是一个待查询的逻辑,因此它是可以被重复使用的。
EF Core分页查询
分页的实现:
1、Skip(x).Take(y)
最好显式指定排列规则;
2、需要知道满足条件的数据的总条数:用 IQueryable的复用;
3、页数:long pageCount = (long)Math.Ceiling(count * 1.0 / pageSize);
。
验证IQueryable用什么方式
IQueryable内部就是在调用DataReader。
优点:节省客户端内存
缺点:如果处理的慢,会长时间占用连接。
如何一次性加载数据到内存
用IQueryable的ToArray()、ToArrayAsync()、ToList()、ToListAsync()等方法。
何时需要一次性加载数据到内存
场景1:遍历IQueryable并且进行数据处理的过程很耗时。
场景2:如果方法需要返回查询结果,并且在方法里销毁DbContext,是不能返回IQueryable的。必须一次性加载返回
场景3:多个IQueryable的遍历嵌套。很多数据库的ADO.NET Core Provider是不支持多个DbContext同时执行的。把连接字符串中的MultipleActiveResultSets=True删掉,其他数据库不支持这个。
EF Core中的异步方法
对应的异步方法:
1、异步方法大部分是定义在Microsoft.EntityFrameworkCore
这个命名空间下EntityFrameworkQueryableExtensions
等类中的扩展方法,记得using;
2、常用异步方法:
AddAsync()
、AddRangeAsync()
、AllAsync()
、AnyAsync()
、AverageAsync()
、ContainsAsync()
、CountAsync()
、FirstAsync()
、FirstOrDefaultAsync()
、ForEachAsync()
、LongCountAsync()
、MaxAsync()
、MinAsync()
、SingleAsync()
、SingleOrDefaultAsync()
、SumAsync()
等
EF Core执行原生SQL语句
1、尽管EF Core已经非常强大,但是仍然存在着无法被写成标准EF Core调用方法的SQL语句,少数情况下仍然需要写原生SQL;
2、可能无法跨数据库;
3、三种情况:非查询语句、实体查询、任意SQL语句查询。
新版本EF Core执行非查询语句时使用 ExecuteSqlInterpolatedAsync()方法,可以有效地规避SQL注入漏洞
好用的IQueryable
1、FromSqlInterpolated()方法的返回值是IQueryable类型的,因此我们可以在实际执行IQueryable之前,对IQueryable进行进一步的处理;
2、把只能用原生SQL语句写的逻辑用FroSqlInterpolated()去执行,然后把分页、分组、二次过滤、排序、Include等其他逻辑尽可能仍然使用EF Core的标准操作去实现。
局限性:
- SQL查询必须返回实体类型对应数据库表的所有列;
- 结果集中的列名必须与属性映射到的列名称匹配;
- 只能单表查询,不能使用Join语句进行关联查询。但是可以在查询后面使用Include()来进行关联数据的获取。
执行任意原生SQL查询语句
啥时候要用ADO.NET
1、FromSqlInterpolated()只能单表查询,但是在实现报表查询等的时候,SQL语句通常是非常复杂的,不仅要多表Join,而且返回的查询结果一般也都不会和一个实体类完整对应。因此需要一种执行任意SQL查询语句的机制。
2、EF Core中允许把视图或存储过程映射为实体,因此可以把复杂的查询语句写成试图或存储过程,然后再声明对应的实体类,并且在DbContext中配置对应的DbSet。
3、不推荐写存储过程;项目复杂查询很多,导致视图太多,非实体的DbSet,DbSet膨胀。
复杂SQL查询用ADO.NET的方式或者Dapper等
EF Core如何知道实体数据变了
快照更改跟踪:
首次跟踪一个实体的时候,EF Core会创建这个实体的快照。执行SavaChanges()等方法时,EF Core将会把存储的快照中的值与实体当前的值进行比较。
实体的状态:
已添加(Added):DbContext正在跟踪此实体,但数据库中尚不存在该实体。
未改变(Unchanged):DbContext正在跟踪此实体,该实体存在于数据库中,其属性和从数据库中读取到的值一致,未发生改变。
已修改(Modified):DbContext正在跟踪此实体,并存在于数据库中,并且其部分或全部属性值已修改。
已删除(Deleted):DbContext正在跟踪此实体,并存在于数据库中,但在下次调用SaveChanges()时要从数据库中删除对应数据。
已分离(Detached):DbContext未跟踪该实体。
DbContext会根据跟踪的实体的状态,在SaveChanges()的时候,根据实体状态的不同,生成Update、Delete、Insert等SQL语句,来吧内存中实体的变化更新到数据库中。
EF Core优化之 AsNoTracking
使用 AsNoTracking() 方法 禁用快照降低内存占用
EF Core全局查询筛选器
1、全局查询筛选器:EF Core 会自动将这个查询筛选器应用于涉及这个实体类型的所有Linq查询。
2、场景:软删除、多租户
用法:
builder.HasQueryFilter(b => b.IsDeleted == false);
忽略查询过滤器:IgnoreQueryFilters()
全局筛选器可能会带来性能问题
EF Core 并发控制
并发控制的概念:
1、并发控制:避免多个用户同时操作资源造成的并发冲突问题
2、最好的解决方案:非数据库解决方案
3、数据库层面的两种策略:悲观、乐观
class Houses
{
public long Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
}
class HouserConfig:IEntityTypeConfiguration<Houses>
{
public void Configure(EntityTypeBuilder<Houses> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsRequired();
}
}
悲观并发控制
1、悲观并发控制一般采用并行锁、表锁等排他锁对资源进行锁定,确保同时只有一个使用者操作被锁定的资源。
2、EF Core没有封装悲观并发控制的使用,需要开发人员编写原生SQL语句来使用悲观并发控制。
代码示例
/*
实现:
锁是和事务相关的,因此通过 BeginTransactionAsync() 创建一个事务,并且在所有操作完成后调用 CommitAsync() 提交事务。
*/
class Program
{
static void Main(string[] args)
{
/* 常规用法 不能避免并发*/
Console.WriteLine("请输入您的名字");
string name = Console.ReadLine();
using(MyDbContext ctx = new MyDbContext())
{
var house = ctx.Houses.Single(h => h.Id == 1);
if(!string.IsNullOrEmpty(house.Owner))
{
if(house.Owner == name)
{
Consle.WriteLine($"房子已经被你抢到了");
}
else
{
Consle.WriteLine($"房子已经被{house.Owner}占了");
}
return;
}
house.Owner = name;
Thread.Sleep(1000 * 5);
Console.WriteLin("恭喜你,抢到了");
ctx.SaveChanges();
}
/* 并发操作 */
Console.WriteLine("请输入您的名字");
string name = Console.ReadLine();
using(MyDbContext ctx = new MyDbContext())
using(var tx = ctx.Database.BeginTransaction())
{
// var house = ctx.Houses.Single(h => h.Id == 1);
var house = ctx.Houses.FromSqlInterpolated($"select * from T_Houses where Id=1 for update").Single();
if(!string.IsNullOrEmpty(house.Owner))
{
if(house.Owner == name)
{
Consle.WriteLine($"房子已经被你抢到了");
}
else
{
Consle.WriteLine($"房子已经被{house.Owner}占了");
}
return;
}
house.Owner = name;
Thread.Sleep(1000 * 5);
Console.WriteLin("恭喜你,抢到了");
ctx.SaveChanges();
tx.Commit();
}
}
}
存在的问题:
锁是独占的、排他的,如果系统并发量很大的话,会严重影响性能,如果使用不当,甚至会导致死锁。
乐观并发控制:并发令牌
原理:
Update T_Houses set Owner="新值" where Id=1 and Owner="旧值"
当 Update的时候,如果数据库中的Owner值已经被其他操作者更新为其它值了,那么where语句的值就会为false,因此这个Update语句影响的行数就是0,EF Core就知道发生“并发冲突”了,因此SaveChanges()方法就会抛出DbUpdateConcurrencyException异常。
代码示例
/* EF Core配置 */
// 1、把被并发修改的属性使用 IsConcurrencyToken() 设置为并发令牌。
// 2、
class HouserConfig:IEntityTypeConfiguration<Houses>
{
public void Configure(EntityTypeBuilder<Houses> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsRequired();
// 配置并发令牌
builder.Property(h => h.Owner).IsConcurrencyToken();
}
}
// 3、
catch(DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = await entry.GetDatabaseValuesAsync();
string newOwner = dbValues.GetValue<string>(nameof(Houses.Owner));
Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");
}
// 用法
class Program
{
static void Main(string[] args)
{
/* 常规用法 不能避免并发*/
Console.WriteLine("请输入您的名字");
string name = Console.ReadLine();
using(MyDbContext ctx = new MyDbContext())
{
var house = ctx.Houses.Single(h => h.Id == 1);
if(!string.IsNullOrEmpty(house.Owner))
{
if(house.Owner == name)
{
Consle.WriteLine($"房子已经被你抢到了");
}
else
{
Consle.WriteLine($"房子已经被{house.Owner}占了");
}
return;
}
house.Owner = name;
Thread.Sleep(1000 * 5);
Console.WriteLin("恭喜你,抢到了");
try
{
ctx.SaveChanges();
Console.WriteLin("恭喜你,抢到了");
}
catch(DbUpdateConcurrencyException ex)
{
var entry = ex.Entries.First();
var dbValues = await entry.GetDatabaseValuesAsync();
string newOwner = dbValues.GetValue<string>(nameof(Houses.Owner));
Console.WriteLine($"并发冲突,被{newOwner}提前抢走了");
}
}
}
}
乐观并发控制:RowVersion
1、SQL Server数据库可以用一个byte[]类型的属性做并发令牌属性,然后使用IsRowVersion()把这个属性设置为RowVersion类型,这样这个属性对应的数据库列就会被设置为ROWVERSION类型。对于ROWVERSION类型的列,在每次插入或更新行时,数据库会自动为这一行的ROWVERSION类型的列生成新值。
2、在SQL Server中,timestamp和rowversion是同一种类型的不同别名而已。
代码示例
```csharp
class Houses
{
public long Id { get; set; }
public string Name { get; set; }
public string Owner { get; set; }
public byte[] RowVer { get; set; }
}
class HouserConfig:IEntityTypeConfiguration<Houses>
{
public void Configure(EntityTypeBuilder<Houses> builder)
{
builder.ToTable("T_Houses");
builder.Property(h => h.Name).IsRequired();
builder.propery(h => h.RowVer).IsRowVersion();
}
}
EF Core 表达式树
什么是表达式树
1、表达式树(Expression Tree):树形数据结构表示代码,以表示逻辑运算,以便可以在运行时访问逻辑运算的结构。
2、Expression<TDelegate>
类型
3、从 Lambda 表达式来生成表达式树:
Expression<Func<Book, bool>> e1 = b => b.Price > 5;
Expression<Func<Book, Book, double>> e2 = (b1, b2) => b1.Price + b2.Price;
查看表达式树的结构
VS内置查看AST
1、Visual Studio中调试程序,然后用【快速监视】的方式查看变量e的值,展开Raw View。
2、整个表达式树是一个 “或” (OrElse) 类型的节点。
3、AST:抽象语法树。
代码查看AST
安装 ExpressionTreeToString
NtGet包
Expression<Func<Book, bool>> e = b => b.Price > 5;
Consle.WriteLine(e.ToString("Object notation", "C#"));
通过代码动态构建表达式树
// 手动构建表达式树
ParameterExpression paramExprB = Expression.Parameter(typeof(Book), "b");
ConstantExpression constExpr5 = Expression.Constant(5.0, typof(double));
MemberExpression memExprPrice = Expression.MakeMemberAccess(paramExprB, typof(Book).GetProperty("Price"));
BinaryExpression binExpGreaterThan = Expression.GreaterThan(memExprPrice, constExpr5);
Expression<Func<Book, bool>> ExprRoot = Expression.Lambda<Func<Book, bool>>(binExpGreaterThan, paramExprB);
using(MyDbContext ctx = new())
{
ctx.Books.Where(ExprRoot).ToArray();
}
1、只有通过代码动态构建表达式树才能更好的发挥表达式树的能力。
2、ParameterExpression、BinaryExpression、MethodCallExpression、ConstantExpression等类几乎都没有提供构造方法,而且所有属性也几乎都是只读的,因此我们一般不会直接创建这些类的实例,而是调用Expression类的Parameter、MakeBinary、Call、Constant等静态方法来生成,这些静态方法我们一般称作创建表达式树的工厂方法,而属性则通过方法参数类设置。
/* 常用工厂方法 */
Add // 加法
AndAlso // 短路与运算
ArrayAccess // 数组元素访问
Call // 方法访问
Condition // 三元条件运算符
Constant // 常量表达式
Convert // 类型转换
GreaterThan // 大于运算
GreaterThanOrEqual // 大于或等于运算符
MakeBinary // 创建二元运算
NoEqual // 不等于运算
OrElse // 短路或运算
Parameter //表达式的参数
// 手动构建表达式树
Console.WriteLine("请选择输出筛选方式,1:大于,2:小于");
string str = Console.ReadLine();
ParameterExpression paramExprB = Expression.Parameter(typeof(Book), "b");
ConstantExpression constExpr5 = Expression.Constant(5.0, typof(double));
MemberExpression memExprPrice = Expression.MakeMemberAccess(paramExprB, typof(Book).GetProperty("Price"));
BinaryExpression binExpCompare
if(str == "1")
{
binExpCompare = Expression.GreaterThan(memExprPrice, constExpr5);
}
else
{
binExpCompare = Expression.LessThan(memExprPrice, constExpr5);
}
Expression<Func<Book, bool>> ExprRoot = Expression.Lambda<Func<Book, bool>>(binExpGreaterThan, paramExprB);
using(MyDbContext ctx = new())
{
ctx.Books.Where(ExprRoot).ToArray();
}
using static System.Linq.EXpressions.EXpression
var b = Parameter(typeof(Book), "b");
var expr = Lambda<Func<Book, bool>>(
GreaterThan(
MakeMemberAccess(b, typeof(Book), GetProperty("Price")),
Constant(5.0)
),
b
)
using(MyDbContext ctx = new())
{
ctx.Books.Where(expr).ToArray();
}
让动态构建表达式树 “动态” 起来
using static System.Linq.Expressions.Expression;
QueryBooks("Price", 18.0);
IEnumerable<Book> QueryBooks(string propName, object value)
{
Type type = typeof(Book);
PropertyInfo propInfo = type.GetProperty(propName);
Type propType = propInfo.PropertyType;
var b = Parameter(typeof(Book),"b");
Expression<Func<Book,bool>> expr;
if (propType.IsPrimitive)//如果是int、double等基本数据类型
{
expr = Lambda<Func<Book, bool>>(Equal(
MakeMemberAccess(b,typeof(Book).GetProperty(propName)),
Constant(value)),b);
}
else//如果是string等类型
{
expr = Lambda<Func<Book, bool>>(MakeBinary(ExpressionType.Equal,
MakeMemberAccess(b,typeof(Book).GetProperty(propName)),
Constant(value), false,propType.GetMethod("op_Equality")
),b);
}
TestDbContext ctx = new TestDbContext();
return ctx.Books.Where(expr).ToArray();
}
尽量避免使用动态构建表达式树