查缺补漏系统学习 EF Core 6 - 软删除与编译查询

这是 EF Core 系列的第八篇,也是系列的最后两篇章节,所以就讲一讲 EF Core 的一些扩展知识:软删除、显式编译查询、IQueryable 和 IEnumerable。

这篇文章就先说一说软删除和显式编译查询。.

软删除

除了直接删除实体,实际业务中还有一个常用的删除模式——「软删除」

软删除本质上是隐藏实体,而不是真正的从数据库中删除数据。

因为在许多实际中的项目里,很多数据都是宝贵的,它们往往可以在以后的一些统计中被用到。

所以基本上,我们不是在删除一个实体,而是通过修改其属性来更新它,这个属性的名称类似 「Deleted」

为了软删除是如何实现的,我们来看这个示例。

首先是 Account 实体类,添加了一个布尔类型的 「Deleted」 属性:

[Table("Account")]
public class Account
{
    [Column("AccountId")]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50, ErrorMessage = "长度必须小于50个字符")]
    public string Name { get; set; }

    public int? Age { get; set; }

    public bool Deleted { get; set; }
}

然后,在配置实体的 AccountConfiguration 类,添加了一个 HasQueryFilter 方法,用来从查询中,过滤掉 「Deleted」 属性为 Ture 的实体,因为这些实体代表已被删除:

public class AccountConfiguration : IEntityTypeConfiguration<Account>
{
    public void Configure(EntityTypeBuilder<Account> builder)
    {
      // 省略其它配置代码
      builder.HasQueryFilter(s => !s.Deleted);
    }
}

现在,我们来做个实验:

var account = _context.Accounts.FirstOrDefault(a=>a.Name == "Zilor");
if (account != null) account.Deleted = true;
_context.SaveChanges();

var accounts = _context.Accounts.ToList();

先删除一个 Acount,然后再查询,可以发现结果只有一个 Account

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

再看下数据库中的数据:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

 

名为 「Zilor」 的数据行其 「Deleted」 属性已被设置为 True,由于我们有一个查询过滤,所以它不会被查询出来。

 

那如果我们想要查询出所有数据,包括已删除的数据,可以忽略过滤,比如这样:

var accounts = _context.Accounts.IgnoreQueryFilters().ToList();

IgnoreQueryFilters 方法用来忽略所查询过滤

此时运行示例,再查看变量的值,可以看到已删除的数据也可以查询出来了。

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

需要说明的是,使用软删除的目的,更多的在于保留历史记录,所以软删除不是必须的功能。

 

因为它也会带来一些问题,尤其是具有导航关系的软删除,尤其要小心处理。

另外,使用软删除,会在你的每一条查询里多出一个筛选条件。

显式编译查询

EF Core 中还有一个显式编译查询的功能。

什么是编译查询呢?

简单来说,就是在查询数据时,预先编译好查询语句,便于在请求数据时能够立即响应。

EF Core 本身使用了查询表达式的散列,来实现自动编译和缓存查询,当我们的代码需要重用以前执行的查询时,EF Core 会使用散列查找,从缓存中返回已编译的查询语句。

但有时候我们可能更希望直接使用编译结果查询,绕过散列计算和缓存查找。

显式编译查询就为我们提供了进一步提高查询性能的可能。

比如我们通过主键查询 Blog,同时使用贪婪加载文章的集合列表,代码如下:

using var context = new BloggingContext();

var blog = context.Blogs
    .AsNoTracking()
    .Include(c => c.Posts)
    .FirstOrDefault(c => c.Id == 1);

当进行查询时,此时要经过编译翻译阶段,最终返回实际结果。

在实际应用中,我们一般会将查询封装为方法来使用,这样就无法优化结果和查询方式。

那么,我们就能够通过编译查询来提前保存好编译结果,以达到缓存的效果。

通过 EF 静态类中的 CompileQuery 扩展方法,可以实现编译查询,我们这里改造一下刚才的查询示例:

var query = EF.CompileAsyncQuery(
  (BloggingContext bloggingContext, int id) =>
       bloggingContext.Blogs
       .Include(c => c.Posts)
       .FirstOrDefault(c => c.Id == id));

var blog1 = query(context, 1).Result;

之后,我们再来测试一下常规查询和显式编译查询的性能。

为了更好的演示,接下来的示例,使用 EF Core 提供的内存库来测试。

因为真实的数据库会进行查询计划优化和缓存,而使用内存数据库就可以避免这些干扰。

首先,测试常规查询,查询一百万次:

using var context = new BloggingContext();

// 常规查询
Func<BloggingContext, Blog> unCompileQuery = (context) =>
{
    return context.Blogs.Include(c => c.Posts)
        .OrderBy(o => o.Id)
        .FirstOrDefault();
};

var stopWatch = new Stopwatch();
stopWatch.Start();
for (var i = 0; i < 1_000_000; i++)
{

    var blog = unCompileQuery(context);
}
stopWatch.Stop();
Console.WriteLine("常规查询:" + stopWatch.Elapsed);

运行程序,可以看到常规查询是 18 秒左右:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

接下来,我们再来看看显式编译查询:

 

using var context = new BloggingContext();

// 编译查询
Func<BloggingContext, Blog> compileQuery = 
  EF.CompileQuery((BloggingContext context) =>
    context.Blogs.Include(c => c.Posts)
        .OrderBy(o => o.Id)
        .FirstOrDefault());

var stopWatch = new Stopwatch();
stopWatch.Start();
for (var i = 0; i < 1_000_000; i++)
{

    var blog = compileQuery(context);
}
stopWatch.Stop();
Console.WriteLine("编译查询:" + stopWatch.Elapsed);

运行程序,可以看到显式编译查询只有 3 秒左右:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

为了得到更清晰的对比,我这里在一次启动中,先后运行这两个测试方法:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

 

 

在当前的这个环境下,编译查询比常规查询快了 6 倍。

 

那么是不是就说明,显式编译查询性能一定优于常规查询呢?

这个问题先不回答,我们继续测试,刚才是一百万次的查询,这次我们降低到一万次:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

从结果中可以看出,差距明显缩小了,只有 2 倍多的差距了。

那如果我们进一步降低查询次数,情况会不会反过来呢?

当然是实践出真知,这次查询次数降低到一千次,再次测试:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询

现在结论出来了,只有当查询基数足够大时,我们才可以使用编译查询来优化性能。

但是还有一个问题,目前显式编译查询并不完善,比如向下面这样的查询就无法实现:

using var context = new BloggingContext();

var query = EF.CompileQuery((BloggingContext bloggingContext) =>
    bloggingContext.Blogs.Include(c => c.Posts)
        .OrderBy(o => o.Id).ToList());

query(context);

执行这个示例查询,会收到一个异常:

查缺补漏系统学习 EF Core 6 - 软删除与编译查询这是因为当前版本的 EF Core,还不支持编译查询返回集合类型,所以使用场景还是很有限的,以后的版本不知道会不会继续完善这个功能。