昨天用EF查数据库,遇到了个很奇怪的问题。linq里并没有left join关键字,也没有LeftJoin的方法,所有join默认都是inner join。那么如果我们想以左为基,不管右侧是否存在都可以查询出一条记录,应该怎么做的?
建数据库太麻烦,这里咱么用集合代替,但是具体写法是一样的。
首先是租客类
public class People
{
public int Id { get; set; }
public string Name { get; set; }
public int HouseId { get; set; }
}
然后是房屋类
public class House
{
public int Id { get; set; }
public string Name { get; set; }
}
然后是Service
public class Service
{
public IEnumerable<People> People => GetAllPeople();
public IEnumerable<House> Houses => GetAllHouse();
private IEnumerable<People> GetAllPeople()
{
var list = new List<People>();
list.Add(new People() { Id = 1, Name = "张三", HouseId = 0 });
list.Add(new People() { Id = 2, Name = "李四", HouseId = 1 });
list.Add(new People() { Id = 3, Name = "王五", HouseId = 2 });
list.Add(new People() { Id = 4, Name = "赵六", HouseId = 3 });
list.Add(new People() { Id = 5, Name = "孙七", HouseId = 4 });
list.Add(new People() { Id = 6, Name = "周八", HouseId = 0 });
list.Add(new People() { Id = 7, Name = "吴九", HouseId = 0 });
list.Add(new People() { Id = 8, Name = "郑十", HouseId = 0 });
return list;
}
private IEnumerable<House> GetAllHouse()
{
var list = new List<House>();
for (int i = 1; i < 9; i++)
{
list.Add(new House() { Id = i, Name = i + "号公寓" });
}
return list;
}
}
万事俱备,接下来开始正题:
var innerjoin = from people in service.People
join house in service.Houses
on people.HouseId equals house.Id
select new { people.Id, people.Name, HouseName = house.Name };
foreach(var item in innerjoin)
{
Console.WriteLine($"{item.Id} {item.Name} {item.HouseName}");
}
输出结果是这样婶儿的:
2 李四 1号公寓
3 王五 2号公寓
4 赵六 3号公寓
5 孙七 4号公寓
然后是重头戏,outer join了。outer join也就是left join(right join在linq里就是左右集合反过来,没有别的办法实现),实现的难点在于让右侧不存在的项,放置一个空即null。
这时候聪明的你开始思考了,为空不就是DefaultIfEmpty()吗,说着随手写出了如下代码:
var innerjoin = from people in service.People
join house in service.Houses.DefaultIfEmpty()
on people.HouseId equals house.Id
select new { people.Id, people.Name, HouseName = house.Name };
结果如何呢?
2 李四 1号公寓
3 王五 2号公寓
4 赵六 3号公寓
5 孙七 4号公寓
???
为什么会这样?
仔细分析就会发现,DefaultIfEmpty()这个方法作用在了service.Houses上,也就是说,只有在我们的service提供的Houses集合为空时,结果才是null,这并不符合我们上面的思路。按照我们的思路,在join右侧的集合的查询结果上调用DefaultIfEmpty()才能生效。
var innerjoin = from people in service.People
join house in service.Houses
on people.HouseId equals house.Id
into tmps
from tmp in tmps.DefaultIfEmpty()
select new { people.Id, people.Name, HouseName = tmp.Name };
结果如下:
1 张三
2 李四 1号公寓
3 王五 2号公寓
4 赵六 3号公寓
5 孙七 4号公寓
6 周八
7 吴九
8 郑十
与上面相比,多了个into。那么这个into是什么意思呢?百度百科可以查到: into 关键字表示 将前一个查询的结果视为后续查询的生成器。
光看这么个玩意儿估计也搞不明白他到底什么意思,接下来我提一个方法你就懂上面那一坨linq关键字都是什么意思了(说实在的,linq关键字真的很反直觉)。
我们把这个linq查询语句改成等价的lambda形式,如下:
var innerjoin = service.People.GroupJoin(inner: service.Houses,
outerKeySelector: people => people.HouseId,
innerKeySelector: house => house.Id,
resultSelector: (people, houses) => new { people.Id, people.Name, Houses = houses.DefaultIfEmpty() })
.SelectMany(collectionSelector: (c => c.Houses),
resultSelector: (c, house) => new { c.Id, c.Name, HouseName = house == null ? "" : house.Name });
是不是一目了然了。join into语句合在一起,相当于GroupJoin方法,将右表整成一个集合,是双层的,形成一个一对多关系;而单纯的join则是平面的,一对一的。
在houses调用DefaultIfEmpty()方法,可以在houses集合为空时,向里面添加一条值为null的项,这样一来就不会被下一步的SelectMany方法忽略。
蓝火牛逼
蓝火牛逼