EF6 SelectMany查询调用ToListAsync报错的GroupJoin改写方案
报错根因
异常触发的核心原因是SelectData方法的写法破坏了EF6的IQueryable查询链路:
- 方法内部在投影匿名对象后提前调用了
.ToList(),这一步会把筛选后的relet表数据全量加载到应用内存,后续的.SelectMany()是对内存集合做LINQ to Objects操作 - 最后调用
.AsQueryable()只是把内存中的IEnumerable包装成了普通IQueryable实例,这个实例没有实现EF6异步查询依赖的IDbAsyncEnumerable<MyModel>接口,也不支持.AsNoTracking()这类EF专属的扩展方法 - 最终在调用
.ToListAsync()时,EF无法识别这个内存包装的IQueryable,直接抛出指定异常。同时这种写法会把大量冗余数据拉到内存,性能很差。
正确实现(EF原生翻译,兼容异步查询)
直接去掉方法内部的.ToList(),用EF6支持的SelectMany重载直接基于导航属性写查询,EF会自动把整个逻辑翻译为SQL的JOIN操作,全程在数据库侧执行,返回的原生EF IQueryable完美支持异步查询和跟踪配置,逻辑和原写法完全一致:
public static IQueryable<MyModel> SelectData(this IQueryable<relet> baseTable) { return baseTable .Where(h => h.table1 != null && h.table1.table2 != null) // 直接关联table3集合展开,EF自动生成对应JOIN SQL .SelectMany(lev => lev.table1.table2.table3, (lev, table3Item) => new MyModel() { ContactKey = new Key { Id = table3Item.Id, Type = table3Item.Type }, Table5Key = lev.table5 != null ? lev.table5.id : null, Table4Key = lev.table1.table2.table4 != null ? lev.table1.table2.table4.id : null }); }
原有GetDataAsync方法不需要做任何修改,链式调用.AsNoTracking()和.ToListAsync()可以正常执行:
public async Task<IEnumerable<MyModel>> GetDataAsync() { return await _context.baseTable.SelectData().AsNoTracking().ToListAsync(); }
显式使用GroupJoin的写法
如果需要手动写GroupJoin实现和上述逻辑一致的关联查询(不依赖级联导航属性的自动关联),可以参考下面的写法,同样是EF可以直接翻译的IQueryable逻辑,支持异步执行:
public static IQueryable<MyModel> SelectData(this IQueryable<relet> baseTable, YourDbContext context) { // 先关联table1 -> table2 -> table3的链路,手动做GroupJoin再SelectMany展开 return from relet in baseTable where relet.table1 != null && relet.table1.table2 != null join t3 in context.Set<table3>() on relet.table1.table2.Id equals t3.table2Id into t3Group from t3Item in t3Group.DefaultIfEmpty() // 左连接保留空table3记录,内连接可去掉该调用 select new MyModel() { ContactKey = new Key { Id = t3Item.Id, Type = t3Item.Type }, Table5Key = relet.table5 != null ? relet.table5.id : null, Table4Key = relet.table1.table2.table4 != null ? relet.table1.table2.table4.id : null }; }
注意:上述手动GroupJoin写法中的关联字段(比如
t3.table2Id)需要和你实际实体配置的外键字段保持一致。写EF查询时,除非明确需要把数据加载到内存做本地逻辑处理,否则不要在IQueryable链路中间调用.ToList()、.ToArray()等立即执行方法,否则会中断EF的查询翻译,引发各类异常和性能问题。
内容的提问来源于stack exchange,提问作者Kamran Shahid
相关产品推荐
相关产品推荐

