这是 EF Core 系列的第四篇文章,上一篇文章讲述了 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 拥有两个导航属性:
AccountDetails
属性是一对一的导航关系;
AccountSubjects
属性是一对多的导航关系。
运行这个简单查询的结果如下:
可以发现,控制台的结果中,两个导航属性的值都是 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
属性:
如果我们想通过 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
服务注入到实体类中,然后修改需要开启懒加载的字段。
需要注意的是,滥用懒加载,会造成性能上的问题。
虽然懒加载只在需要读取关联数据的时候才进行加载,但是如果在遍历中使用的话,每次读取一条数据,那么就会查询一次数据库,增加了访问数据库的次数,会导致数据库的压力增大。
贪婪加载也一样会有性能上的问题,因为一次性读取所有相关的数据,有可能会导致部分数据在实际上用不到,从而使查询数据的效率降低。
所以,我们应该清楚什么时候应该使用哪种加载方式:
如果在开发时不确定是否会需要相关联的数据,那么可以选择懒加载,待确定需要后再加载它。
如果在开发时就可以预见,需要一次性加载所有的数据,而且需要包含导航关系的所有数据, 那么使用贪婪加载是比较好的选择。
更多精彩内容,请关注我▼▼
如果喜欢我的文章,那么
在看和转发是对我最大的支持!
(戳下面蓝字阅读)
ASP.NET 6 中间件系列
查缺补漏系统学习 EF Core 6 系列
推荐关注微信公众号:码侠江湖
觉得不错,点个在看再走哟