EF6学习笔记三十二:性能优化——实体缓存和翻译缓存
要专业系统地学习EF推荐《你必须掌握的Entity Framework 6.x与Core 2.0》。这本书作者(汪鹏,Jeffcky)的博客:https://www.cnblogs.com/CreateMyself/
这次的学习体验,从极为平常的代码中引出了两个高大上的概念——实体缓存和翻译缓存。真的觉得简单只是表面现象。
实体缓存
EF中提供了一个Find方法,我们来看一下这个方法的注释
翻译结果如下:
查找具有给定主键值的实体。如果一个实体具有给定的
主键值存在于上下文中,然后立即返回
向商店发出请求。否则,将向存储发出请求
附加一个具有给定主键值的实体,如果找到该实体,则附加该实体
返回上下文并返回。如果在上下文中或存储中没有找到实体,
然后返回null。
如果说该主键的实体在上下文中已经存在,那么再次查询就会直接返回,不会再向数据库提交查询。
using (EFDbContext ctx = new EFDbContext()) { ctx.Database.Log = msg => Console.WriteLine($"----------{msg}"); var stu1 = ctx.Students.Find(1); var stu2 = ctx.Students.Find(1); }
通过打印日志我们可以看到,只执行了一次查询
但是firstOrDefault方法就不会,比如我执行两次firstOrDefault,那么EF会执行两次查询
var stu1 = ctx.Students.FirstOrDefault(x => x.Id == 1); var stu2 = ctx.Students.FirstOrDefault(x => x.Id ==1);
那么如果是这样
var stu1 = ctx.Students.FirstOrDefault(x => x.Id == 1); var stu2 = ctx.Students.Find(1);
可以看到也只执行了一次查询,这也符合Find的要求,当Find来查询的时候,发现该实体已经在上下文存在就直接返回了。
那么如果我们交换一下位置,这样来。显然不符合Find的要求,会执行两次查询。
var stu2 = ctx.Students.Find(1); var stu1 = ctx.Students.FirstOrDefault(x => x.Id == 1);
这里我们发现FisrtOrDefault生成的SQL语句包含"TOP1",而Find生成的SQL语句却是“TOP2”
这是什么问题,来看一下SingleOrDefault这个方法,这个方法如果查询不到数据或者查询到一条一上的数据就会报错,它要保证数据是唯一的,所以会有TOP2
但是Find为什么是TOP2?Find方法时按照主键进行查询的,不可能查询到多条数据。
看一下Find方法接受的参数,是params object[] 类型的,为什么?联合主键!但是联合主键也不可能查询出多个啊,我们来看一下
联合主键这个真的是太冷门了,我一次都没用过,来看看怎么创建,我今天将SQL语句、DataAnnotations、Fulent API三种方式创建联合主键都Get到了
SQL语句,先创建一张没有主键的表
create table tb_test1 ( id1 int not null, id2 int not null, [name] nvarchar(10) )
-- 传入的两列不能是主键,不然报错,说该表已经存在主键 alter table tb_test1 add constraint pk_id2 primary key(id1,id2)
DataAnnotation
public class Student3 { [Key,Column(Order =1)] public int Id1 { get; set; } [Key,Column(Order = 2)] public int Id2 { get; set; } public string Name { get; set; } }
Fluent API
public class Student4 { public int Id1 { get; set; } public int Id2 { get; set; } public string Name { get; set; } }
modelBuilder.Entity<Student4>().ToTable("tb_Students4") .HasKey(x => new { x.Id1, x.Id2 });
DataAnnotaion的方式必须要加Column.Order,不然就会报下面这个错误。但是Fluent API方式根本就不需要配置ColumnOrder也可以。
那这个ColumnOrder到底是什么意思。其实就是指定列的顺序,比如Id1属性的ColumnOrder为9,Name属性的ColumnOrder属性为1,那么最后生成的表结构就是,Name列在Id1列的左边,Name列先于Id1列创建。
这个还是不错的吧,我以前也碰到过这样的情况,就是我用SQL语句给某张表添加了一列,默认是在最后面。怎么让这个列在固定的位置了,我百度了很多都没有结果。没想今天又碰到这个问题。
肯定不是因为联合主键之间有什么主从关系才用到ColumnOrder,联合主键之间不存在主从关系,他们都是平等的。
联合主键创建成功,可以看到表设计里面,有两把黄钥匙
联合主键应该说的不全面,可能还有不同表之间的联合主键,这里纯粹只是想制造更多的情况来看看Find方法的执行情况。
然后用Find查询,可以
var stu = ctx.Students4.Find(new object[] { 1, 1 });
如果说我只传递一个值呢?会报错,他说,有几个主键就要传递几个
未经处理的异常: System.ArgumentException: The number of primary key values passed must match number of primary key values defined on the entity.
如果说我什么都不传,也是上面这个错误。那我就想到了,我可以创建一张没有主键的表,然后用Find来查询,是不是会查询到多条数据?
但是不行,EF不能够创建没有主键的表。
所以Find方法为什么生成的SQL包含TOP2的问题就不知道了。
但是我们认识到实体缓存的概念,这个很有用。
翻译缓存
EF转换成LINQ ToEntities需要两步。第一步,将LINQ表达式树变异成数据库表达式树;第二步,将数据库表达式树生成SQL语句。
我们要明白一点:EF总是编译LINQ表达式树到数据库表达式树并生成SQL缓存秘钥存在字典中。
什么意思?就是LINQ表达式翻译成的SQL语句缓存起来,如果其他的执行相同那么就可以直接复用了,不用在编译成SQL了。
来看一下下面两个简单的查询
var stu = ctx.Students.Where(x => x.Id == 1); var stu2 = ctx.Students.Where(x => x.Id == 2);
他们生成的SQL语句分别如下
SELECT [Extent1].[Id] AS[Id], [Extent1].[Name] AS[Name], [Extent1].[Score] AS[Score], [Extent1].[AddTime] AS[AddTime] FROM[dbo].[tb_Students] AS[Extent1] WHERE 1 = [Extent1].[Id]
SELECT [Extent1].[Id] AS[Id], [Extent1].[Name] AS[Name], [Extent1].[Score] AS[Score], [Extent1].[AddTime] AS[AddTime] FROM[dbo].[tb_Students] AS[Extent1] WHERE 2 = [Extent1].[Id]
我们给where方法传递的是常量值,第一个查询生成的SQL语句不能被第二个复用,我们只需要将条件以变量的方式传递进去就可以翻译复用了
int id = 1; int id2 = 2; var stu = ctx.Students.Where(x => x.Id == id); var stu2 = ctx.Students.Where(x => x.Id == id2);
SELECT [Extent1].[Id] AS[Id], [Extent1].[Name] AS[Name], [Extent1].[Score] AS[Score], [Extent1].[AddTime] AS[AddTime] FROM[dbo].[tb_Students] AS[Extent1] WHERE[Extent1].[Id] = @p__linq__0
这里只是翻译复用,并不是他会减少查询次数,这个是减轻LINQ表达式树编译成数据库表达式树的压力
作者说:EF将缓存存在Microsoft.Extensions.Caching.Memory.MemoryCache中。
但是,不是。这个是.NET Core里面的命名空间
我也不知道在.NET Framework是哪个命名空间,这个或许可以查看具体是个什么情况。
因为我有这样的疑问,EF是缓存的所有SQL翻译,还是模板?
比如 select * from tb_students where id =1
我说的模板是:select * from tb_students whrere id =变量
那么翻译是在什么时候缓存的?是在运行到LINQ时,还是在执行查询时?这些疑问目前还没有解决
传递变量的情况也有例外,比如我们在调用Skip、Take方法时,传递变量会发现生成的SQL语句里面还是常量。
int s = 999; int t = 999; var stu = ctx.Students.OrderBy(x => x.Id).Skip(s).Take(t); Console.WriteLine(stu);
SELECT [Extent1].[Id] AS[Id], [Extent1].[Name] AS[Name], [Extent1].[Score] AS[Score], [Extent1].[AddTime] AS[AddTime] FROM[dbo].[tb_Students] AS[Extent1] ORDER BY row_number() OVER(ORDER BY [Extent1].[Id] ASC) OFFSET 999 ROWS FETCH NEXT 999 ROWS ONLY
对于这种情况也是有现成的解决办法的,那就是使用扩展后的Skip\Take方法,这个需要引用System.Data.Entity命名空间
int s = 999; int t = 999; var stu2 = ctx.Students.OrderBy(x => x.Id).Skip(() => s).Take(() => t); Console.WriteLine(stu2);
SELECT [Extent1].[Id] AS [Id], [Extent1].[Name] AS [Name], [Extent1].[Score] AS [Score], [Extent1].[AddTime] AS [AddTime] FROM [dbo].[tb_Students] AS [Extent1] ORDER BY row_number() OVER (ORDER BY [Extent1].[Id] ASC) OFFSET @p__linq__0 ROWS FETCH NEXT @p__linq__1 ROWS ONLY