查缺补漏系统学习 EF Core 6 - 数据查询

这是 EF Core 系列的第四篇文章,上一篇文章讲述了 EF Core 中的实体迁移与数据播种。

这篇文章盘点一下 EF Core 的几种数据查询方式,内容较多分上下两篇。

简单查询

在 EF Core 中,每个查询都由三个主要部分组成:.

  • 通过 ApplicationContext 的 DbSet 属性连接到数据库
  • 使用一系列的 LINQ 或 EF Core 命令
  • 执行查询

这是一个最简单的示例:

public void Run()
{
    var accounts = 
    _context.Accounts
        .Where(s => s.Age > 16)
        .ToList();
}

从这个查询中,我们可以看到查询的三个主要部分:

「_context.Accounts」 是查询第一部分,通过 DbSet<Account> 属性,访问数据库中的 Account 表。

「Where(s => s.Age > 25)」 是查询的第二部分,使用 LINQ 方法筛选需要的行。

最后,「ToList()」 方法用来来执行这个查询。

需要注意的是,当我们在 EF Core 中编写只读查询时,可以添加 AsNoTracking 方法提高查询效率:

_context.Accounts.AsNoTracking()

使用 AsNoTracking 方法时,EF Core 不会跟踪加载实体的变化。

关系型查询

在 EF Core 查询导航属性(表关联字段)的方式有多种:「贪婪加载」「显式加载」「懒惰加载」

贪婪加载

贪婪加载也叫预先加载。

所谓贪婪加载,就是在查询结果中包含导航关系,而这就需要明确的要求。

比如这个示例中,Account 拥有两个导航属性:

查缺补漏系统学习 EF Core 6 - 数据查询

AccountDetails属性是一对一的导航关系;

AccountSubjects属性是一对多的导航关系。

运行这个简单查询的结果如下:

查缺补漏系统学习 EF Core 6 - 数据查询

可以发现,控制台的结果中,两个导航属性的值都是 Null

在 EF Core 中,只有明确要求的情况下,才会在结果中包含导航关系。这个简单查询中,没有明确要求包含导航关系。

如果使用贪婪加载,可以让 EF Core 在查询结果中包含导航属性的值。

贪婪加载通过使用 Include() 和 ThenInclude() 方法实现,如下所示:

var accounts = 
  _context.Accounts
    .Include(e => e.AccountSubjects)
    .Where(s => s.Age > 16)
    .ToList();

Include 方法用来加载第一层导航关系,如果想进一步加载导航关系呢?

比如 AccountSubjects 属性中有两个一对一导航,分别是 Accout 属性和 Subject 属性:

查缺补漏系统学习 EF Core 6 - 数据查询

如果我们想通过 AccountSubjects 导航属性,进一步查询出 Subject 属性,就可以这么做 :

var accounts = _context.Accounts
    .Include(e => e.AccountSubjects)
    .ThenInclude(s => s.Subject)
    .Where(s => s.Age > 16)
    .ToList();

ThenInclude 方法用来进一步加载导航关系。该方法可以无限递进非关系深度,如果关系不存在,查询也不会失败,只是不会返回任何东西。

「贪婪加载的优点是,以一种高效的方式,查询了关系型数据,使用了最少的数据库访问次数;」

「它的缺点是,一次性加载了所有的数据,即使我们不需要其中的某些数据。」

显式加载

所谓显式加载,就是 EF Core 显式地将关系,加载到已经加载的实体中。

比如这个示例:

var account = _context.Accounts.FirstOrDefault();

_context.Entry(account)
    .Collection(ss => ss.AccountSubjects)
    .Load();

foreach (var accountSubject in account.AccountSubjects)
{
    _context.Entry(accountSubject)
        .Reference(s => s.Subject)
        .Load();
}

我们首先加载的是 Acount 实体,然后通过 AccountSubjects 导航属性关联所有相关的子项。

在这种情况下,Acount 实体被称为主实体。

Collection 方法可以把一个集合纳入主实体,Reference 方法可以把单一的实体纳入主实体。

Account 实体通过使用 Collection 方法,包含了 AccountSubjec 集合。

AccountSubject 实体通过使用 Reference 方法,包含了 Subject 实体。

使用显式加载时,除了 Load 加载方法,还可以使用查询方法,它允许将查询应用到关系中:

var count = _context.Entry(account)
    .Collection(a => a.AccountSubjects)
    .Query()
    .Count();

var subjects = _context.Entry(account)
    .Collection(a => a.AccountSubjects)
    .Query()
    .Select(s => s.Subject)
    .ToList();

「显式加载的好处是,只有当真正需要的时候,我们才会在实体类上加载一个导航关系。」

另一个好处是,如果我们有复杂的业务逻辑,那就可以分别加载导航关系。

另外,导航关系加载可以封装到一个方法、甚至是一个类中,从而使代码更容易阅读和维护。

不过,这种方法的缺点是,会产生更多的数据库查询次数,来加载所有需要的关系,会降低查询的效率。

懒惰加载

懒加载也叫延迟加载、按需加载,它和贪婪加载相反,顾名思义,暂时不需要的数据就不加载,而是推迟到使用它时再加载。

延迟加载是一个比较重要的数据访问特性,它可以有效地减少与数据源的交互。

注意,这里所指的交互不是指交互次数,而是指交互的数据量。

EF Core 中默认是不开启这个功能的,因为在使用不当的情况下,它会降低应用的性能。

想要使用懒加载,最简单的办法就是安装 Microsoft.EntityFrameworkCore.Proxies 库,使用代理模式实现懒加载。

在上下文类的配置方法中启用懒加载代理:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder.UseLazyLoadingProxies();
}

配置完成后,EF Core 会为任何可以被重载的导航属性,启用懒惰加载。

需要注意的是,这是一种全局配置,所有的导航属性都必须使用 virtual 修饰,否则会发生异常错误。

不过,这样一来的话,所有的导航属性都默认启用了懒加载。

除了使用代理模式,还可以使用 EF Core 中的懒加载服务,这种方式不需要用 virtual 修饰导航属性,而且可以只针对特定实体进行懒加载。

具体来看示例:

public class Account
{
    private readonly ILazyLoader _lazyLoader;

    public Account(ILazyLoader lazyLoader)
    {
        _lazyLoader = lazyLoader;
    }
 
    private ICollection<AccountSubject> _accountSubjects;
    public ICollection<AccountSubject> AccountSubjects
    {
        get => _lazyLoader?.Load(this, ref _accountSubjects);
        set => _accountSubjects = value;
    }

}

使用构造函数注入的方式,将 ILazyLoader 服务注入到实体类中,然后修改需要开启懒加载的字段。

需要注意的是,滥用懒加载,会造成性能上的问题。

虽然懒加载只在需要读取关联数据的时候才进行加载,但是如果在遍历中使用的话,每次读取一条数据,那么就会查询一次数据库,增加了访问数据库的次数,会导致数据库的压力增大。

贪婪加载也一样会有性能上的问题,因为一次性读取所有相关的数据,有可能会导致部分数据在实际上用不到,从而使查询数据的效率降低。

所以,我们应该清楚什么时候应该使用哪种加载方式:

如果在开发时不确定是否会需要相关联的数据,那么可以选择懒加载,待确定需要后再加载它。

如果在开发时就可以预见,需要一次性加载所有的数据,而且需要包含导航关系的所有数据, 那么使用贪婪加载是比较好的选择。