C# Linq中的outer join

日期 2019-01-14 默认分类,C# 作者 叫我蓝火火 共2评论

昨天用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方法忽略。

已有 2 条评论
如果有错误请告诉我,谢谢各位大佬