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

 

posted @ 2019-02-23 22:41  张四海  阅读(407)  评论(0编辑  收藏  举报