在实体框架核心中返回多个相关实体的最有效查询?(1:M和M:M)



我在实体框架核心5中有一个数据库上下文,其中包含锦标赛、比赛、集合、玩家的表,以及一个集合-玩家连接表,用于关联多对多关系。链接的ERD图像包含详细信息。我想使用LINQ来查询锦标赛ID,并返回匹配锦标赛的所有相关实体(包括比赛、集合、玩家(。

ERD(编辑:我刚刚意识到我在图表上错误地命名了一个键和元素——应该是SetId和SetPlayer——尽管这一点一看就会很明显。(

代码首先是通过使用EF CLI脚手架工具对数据库进行反向工程生成的,导航属性都设置正确。

我目前正在使用多个单独的查询:

var tournament = _context.Tournaments.FirstOrDefaultAsync(t => t.Id == id);
var matches = await _context.Matches.Where(r => r.TournamentId == tournamentId);
var sets = ...etc

这肯定是效率低下的,在SQL中可以简单地编写一个带有多个JOIN的SELECT*。如何使用EF Core制定LINQ语句以返回我需要的所有元素?

我已经做了大量的研究和实验,但我仍然很难弄清楚如何做到这一点。这里尝试至少获取第一个相关联的元素,但我显然不是在查询我需要的数据。

var tournament = await _context.Tournaments
.Include(t => t.Matches)
.ThenInclude(t => t.Sets)
.ThenInclude(t => t.SetPlayer)
.ThenInclude(t => t.Players)

如有任何协助,我们将不胜感激。

如果设置了导航属性:

var tournament = await _context.Tournament
.Include(t => t.Matches).ThenInclude(m => m.Sets)
.Include(t => t.Matches)
.ThenInclude(m => m.MatchPlayers).ThenInclude(m => m.Players)
.ToArrayAsync();

请注意,.Include(t => t.Matches)为每个子关系调用了两次。

实际上,异步执行的多个不同查询可能比一个大查询更高效。

根据您的需要,有几个选项。如果您需要锦标赛的一些详细信息及其相关数据以返回视图,例如,使用Select()或Automapper的ProjectTo()进行投影是您最好的朋友。返回实体很混乱,我不建议这样做,尽管有很多官方例子证明了这一点。返回实体是一个陷阱,因为它发送的数据远远超过了客户端需要的数据,即使通过Include()加载,随着系统的发展引入了新的关系,它们引入了性能陷阱,因为序列化程序开始延迟加载旧视图不需要的相关数据,因为导航属性现在就在那里。(从来没有回到.Include()又一个相关实体(为一些新的功能/屏幕添加一个相关的表,发现用户抱怨几个月没碰过的一些不相关的搜索屏幕是"不相关的";"慢";。

用比赛、布景和玩家投影锦标赛视图模型:

var tournamentSummary = await context.Tournaments
.Where(t => t.TournamentId == tournamentId)
.Select(t => new TournamentSummaryViewModel
{
TournamentId = t.TournamentId,
Name = t.Name,
Matches = t.Matches.Select(m => 
new MatchSummaryViewModel
{
//... Fill details from match.
Sets = m.Sets.Select(s => 
new SetSummaryViewModel
{
//... Fill in details from set.
}).ToList()
Players = m.MatchPlayers.Select(mp =>
new PlayerSummaryViewModel
{
PlayerId = mp.Player.PlayerId,
Name = mp.Player.Name,
// ...
}).ToList()
}
}).SingleAsync();

这看起来像是一个复制数据的样板,但使用Automapper可以大大简化它,配置在相关实体之间映射的内容和方式,并将其简化为一条语句:

var tournamentSummary = await context.Tournaments
.Where(t => t.TournamentId == tournamentId)
.ProjectTo<TournamentSummaryViewModel>(TournamentToSummaryMapping.Config)()
.SingleAsync();

其中TournamentToSummaryMapping是已配置的Automapper配置的注入包装。

如果你需要一个完整的实体图,那么你有几个选项。对于具有导航属性的相关数据,可以按照Fabio的示例排列.Include.ThenInclude

如果您需要一组不相关的数据,而这些数据没有导航属性,那么您几乎需要按照概述加载它们。当谈到使用async/await时,我只建议将其用于重要的读取操作(复杂(或非常高的卷操作。awaited代码的线程切换会带来成本,而这对于简单、快速的查询来说往往是不合理的。

var match = await context.Matches.SingleAsync(x => x.MatchId == matchId);
var sets = await context.Sets.Where(x => x.MatchId == matchId).ToListAsync();
var matchPlayers = await context.MatchPlayers.Where(x => x.MatchId == matchId).ToListAsync();

你不能做的是:

var matchTask = context.Matches.SingleAsync(x => x.MatchId == matchId);
var setsTask = context.Sets.Where(x => x.MatchId == matchId).ToListAsync();
var matchPlayersTask = context.MatchPlayers.Where(x => x.MatchId == matchId).ToListAsync();
await Task.WhenAll(matchTask, setsTask, matchPlayersTask);
var match = matchTask.Result;
var sets = setsTask.Result;
var matchPlayers = matchPlayersTask.Result;

EF将发出嘶嘶声,试图在单个上下文上并行化。通过确保每个任务方法都有自己的DbContext实例,您可以使用EF并行化数据检索,但请记住,返回的任何实体引用都将超出读取它们的DbContext的范围(在任务完成时终止(,因此它们不能延迟加载,并且如果不首先检查具有相同ID的被跟踪实体的上下文实例,就不能保证这些实体可附加到另一个DbContext。请极其小心地探索。虽然它可以节省几秒钟的加载时间,但它可能会让你花费数天的时间来增加复杂性,并追踪奇怪的、间歇性的bug。

通常,通过为Projection结构化或保持更新/插入操作更原子化,可以充分解决性能问题,这样就不会一次更新整个数据图。

最新更新