EF Core 3.1.9关联查询CariHareket重复行及分组支持缺失问题
这是EF Core在关联多个导航集合时的典型笛卡尔积问题,我来给你拆解原因和可行的解决方案:
问题根源
当你同时IncludeKontrat下的多个子集合(比如KontratKalemler、KontratTarihBaglantilar这些),EF Core会生成一张多表Join的SQL,子集合的行数相乘就会把主表CariHareket的记录重复返回。而且EF Core 3.1确实不支持直接对带导航属性的查询用GroupBy去重,得换思路处理。
先回顾下你的查询代码和实体定义:
当前查询代码
hareketService.GetAll(Predicate())//表名:CariHareket .Include(s => s.Cari) .Include(s => s.HedefHareketCariVirman) .Include(s => s.HareketCari) .Include(s => s.Kontrat).ThenInclude(s => s.KontratKalemler) .Include(s => s.Kontrat).ThenInclude(s => s.KontratTarihBaglantilar) .Include(s => s.Kontrat).ThenInclude(s => s.FaturaTalimativeTurkceFatura) .Include(s => s.Kontrat).ThenInclude(s => s.AraciCari)
关键实体定义
public class CariHareket { [Key] public int ID { get; set; } [ForeignKey(nameof(CariID))] public virtual Cariler Cari { get; set; } public int? CariID { get; set; } [ForeignKey(nameof(KontratID))] public virtual KontratUst Kontrat { get; set; } public int? KontratID { get; set; } [ForeignKey(nameof(HareketCariID))] public virtual Cariler HareketCari { get; set; } public int? HareketCariID { get; set; } [ForeignKey(nameof(HedefHareketCariVirmanID))] public virtual Cariler HedefHareketCariVirman { get; set; } public int? HedefHareketCariVirmanID { get; set; } // 其他属性... } public class Kontrat { // 其他属性... [InverseProperty("Kontrat")] public virtual BindingList<KontratMasraflarNotlar> KontratMasraflarNotlar { get; set; } [InverseProperty("Kontrat")] public virtual BindingList<KontratKalem> KontratKalemler { get; set; } [InverseProperty("Kontrat")] public virtual BindingList<KontratTarihBaglantilar> KontratTarihBaglantilar { get; set; } [InverseProperty("Kontrat")] public virtual BindingList<KontratTurkceFaturaKalem> FaturaTalimativeTurkceFatura { get; set; } // 其他子集合... }
可行解决方案
方案1:用拆分查询(Split Queries)最简单
EF Core 3.1从3.0开始支持拆分查询,它会为每个导航集合单独生成SQL,彻底避免笛卡尔积。只需要在查询末尾加AsSplitQuery():
hareketService.GetAll(Predicate()) .Include(s => s.Cari) .Include(s => s.HedefHareketCariVirman) .Include(s => s.HareketCari) .Include(s => s.Kontrat).ThenInclude(s => s.KontratKalemler) .Include(s => s.Kontrat).ThenInclude(s => s.KontratTarihBaglantilar) .Include(s => s.Kontrat).ThenInclude(s => s.FaturaTalimativeTurkceFatura) .Include(s => s.Kontrat).ThenInclude(s => s.AraciCari) .AsSplitQuery(); // 加这一句就行
EF Core会自动拆分多个查询,然后在内存里把数据组合起来,不会返回重复的CariHareket记录,这个方案最省心。
方案2:手动分步加载(性能最优)
如果拆分查询不符合你的场景(比如担心多查询的开销),可以分三步加载数据:
// 1. 先拉主表数据,只加载Kontrat主实体,不加载子集合 var cariHareketler = hareketService.GetAll(Predicate()) .Include(s => s.Cari) .Include(s => s.HedefHareketCariVirman) .Include(s => s.HareketCari) .Include(s => s.Kontrat) // 只加载Kontrat本身,不包含子集合 .ToList(); // 2. 提取所有需要的KontratID,批量加载所有子集合 var kontratIds = cariHareketler .Where(ch => ch.KontratID != null) .Select(ch => ch.KontratID.Value) .Distinct() .ToList(); var kontratsWithDetails = _context.Kontrats .Include(k => k.KontratKalemler) .Include(k => k.KontratTarihBaglantilar) .Include(k => k.FaturaTalimativeTurkceFatura) .Include(k => k.AraciCari) .Where(k => kontratIds.Contains(k.ID)) .ToList(); // 3. 把加载好的Kontrat关联回主集合 foreach (var ch in cariHareketler) { if (ch.KontratID != null) { ch.Kontrat = kontratsWithDetails.FirstOrDefault(k => k.ID == ch.KontratID); } }
这种方式完全由你控制数据加载,没有笛卡尔积,数据量大时性能比拆分查询更好。
方案3:内存中去重(适合小数据集)
如果你的数据量不大,可以先把查询结果拉到内存,再用DistinctBy去重。EF Core 3.1本身没有DistinctBy,可以自己实现或者引入System.Linq.Dynamic.Core包:
var cariHareketler = hareketService.GetAll(Predicate()) .Include(s => s.Cari) .Include(s => s.HedefHareketCariVirman) .Include(s => s.HareketCari) .Include(s => s.Kontrat).ThenInclude(s => s.KontratKalemler) .Include(s => s.Kontrat).ThenInclude(s => s.KontratTarihBaglantilar) .Include(s => s.Kontrat).ThenInclude(s => s.FaturaTalimativeTurkceFatura) .Include(s => s.Kontrat).ThenInclude(s => s.AraciCari) .AsEnumerable() // 转为内存集合 .DistinctBy(ch => ch.ID) // 按主键去重 .ToList();
注意:这个方案会先把所有重复数据加载到内存,大数据量时会吃内存,谨慎使用。
为什么GroupBy不行?
EF Core 3.1对GroupBy的支持很有限,当查询包含导航属性时,GroupBy会被转为客户端评估,而且无法正确保留导航集合的完整数据,所以确实不能用它来解决这个重复问题。上面的三个方案都是更可靠的替代方案。
内容的提问来源于stack exchange,提问作者lkmcelik

