EF Core LINQ如何实现带关联条件左连接 保留左表无匹配记录
容易混淆的错误写法
首先看一段常见的SQL写法:
select * from car left join parts ON car.Id = parts.carId where parts.MemberId = 1
很多人在已经用car.Include(x => x.parts)预加载Parts关联数据的前提下,会把上面的SQL写成如下LINQ:
queryable = queryable.Where(x => x.parts.Select(y => y.MemberId).Contains(1) );
这两种写法效果完全一致:最终只会返回存在MemberId为1的配件的车辆,本质上因为where条件的存在,左连接退化成了内连接,没有对应匹配配件的车辆行都会被过滤掉。
目标需求
我们需要实现真正的左连接逻辑:返回car表的所有行,即使parts表中没有对应MemberId=1的匹配记录,对应的车辆行也要保留,对应的原生SQL是把配件过滤条件直接写在left join的on子句中,而非外层where:
select * from car left join parts ON car.Id = parts.CarId and parts.MemberId = 1
涉及的实体模型定义如下(修正了原定义里的属性缺省笔误):
public class Car { public int Id { get; set; } public virtual ICollection<Part> Parts { get; set; } } public class Part { public int Id { get; set; } public int CarId { get; set; } public virtual Car Car { get; set; } public int MemberId { get; set; } }
正确LINQ写法
方法1:过滤Include(EF Core 5.0+ 推荐)
EF Core 5.0及以上版本直接支持在Include中对关联集合做过滤,这个操作不会影响左表(Car)的查询结果,只会过滤关联的Parts集合数据,生成的SQL和目标SQL完全等效:
var result = _dbContext.Cars .Include(c => c.Parts.Where(p => p.MemberId == 1)) .ToList();
返回结果中,所有Car对象都会被保留,每个Car的Parts集合里只会存在MemberId=1的配件,没有匹配配件的Car对应的Parts集合是空集合。
方法2:显式左连接写法(兼容所有EF Core版本)
如果使用的是更低版本的EF Core不支持过滤Include,可以用标准的GroupJoin + DefaultIfEmpty的左连接语法实现:
var result = (from car in _dbContext.Cars join part in _dbContext.Parts.Where(p => p.MemberId == 1) on car.Id equals part.CarId into matchedParts from part in matchedParts.DefaultIfEmpty() select new { CarInfo = car, MatchedPart = part }).ToList();
这种写法返回的结果中,没有匹配配件的车辆对应的MatchedPart字段为null,所有车辆信息都会保留。
注意:千万不要把Parts的过滤条件写在外层的Where中,否则就会退化成开头提到的内连接效果,丢失无匹配记录的车辆行。
内容的提问来源于stack exchange,提问作者jamheadart

