CodeGym /课程 /C# SELF /group join做分组关联

group join做分组关联

C# SELF
第 33 级 , 课程 1
可用

1. 入门

想象下经典场景:你有两个集合,它们逻辑上有关联。比如商品分类列表和商品列表。你要为每个分类拿到这个分类下的所有商品。或者有公司部门列表和员工列表,要输出每个部门的所有员工。

在SQL里这叫“分组连接”(GROUP JOIN,更准确点是LEFT OUTER JOIN加分组)。LINQ里有专门的操作符——GroupJoin。它介于普通的Join(每个左边的记录只连一个右边的)和按key分组之间。GroupJoin会把一个集合的每个元素,和另一个集合里所有相关的元素作为集合连起来。

类比

如果普通Join像是按姓氏把“爸爸和儿子”配成对,GroupJoin就是建一棵树:每个爸爸下面挂上所有他的孩子。

图示


           分类                   商品
        +--------------+         +---------------------+
        | Id | 名称    |         | 名称      | CatId   |
        +----+---------+         +-----------+---------+
        | 1  | 面包    |   --->  | 法棍     | 1       |
        | 2  | 饮料    |         | 香肠     | 3       |
        | 3  | 肉类    |         | 百事可乐 | 2       |
        |    |         |         | 茶        | 2       |
        +----+---------+         +-----------+---------+

GroupJoin后:

  • 面包 — [法棍]
  • 饮料 — [百事可乐, 茶]
  • 肉类 — [香肠]

2. 方法签名和基础概念

扩展方法


public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
    this IEnumerable<TOuter> outer,                   // “外部”集合(比如分类)
    IEnumerable<TInner> inner,                        // “内部”集合(比如商品)
    Func<TOuter, TKey> outerKeySelector,              // 怎么从外部元素拿key
    Func<TInner, TKey> innerKeySelector,              // 怎么从内部元素拿key
    Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // 结果对象/记录的工厂
)
  • outer: 要遍历的集合,其他元素会连到它(比如分类)。
  • inner: 要选出要连的元素的集合(比如商品)。
  • outerKeySelector: 返回“左边”元素key的lambda。
  • innerKeySelector: 返回“右边”元素key的lambda。
  • resultSelector: 决定每对(左+右组)结果长啥样的函数。

3. 实战例子:分类和商品

假设我们有如下模型:


public class Category
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class Product
{
    public string Name { get; set; }
    public int CategoryId { get; set; }
}

例子里的集合:


var categories = new List<Category>
{
    new Category { Id = 1, Name = "面包" },
    new Category { Id = 2, Name = "饮料" },
    new Category { Id = 3, Name = "肉类" }
};

var products = new List<Product>
{
    new Product { Name = "法棍", CategoryId = 1 },
    new Product { Name = "百事可乐", CategoryId = 2 },
    new Product { Name = "茶", CategoryId = 2 },
    new Product { Name = "香肠", CategoryId = 3 }
};

GroupJoin(方法语法)


var groupJoin = categories.GroupJoin(
    products,
    category => category.Id,                    // 分类key
    product => product.CategoryId,              // 商品key
    (category, prods) => new                   // 直接构造结果
    {
        CategoryName = category.Name,
        Products = prods.Select(p => p.Name).ToList() // 该分类下商品名列表
    }
);

怎么遍历结果:


foreach (var group in groupJoin)
{
    Console.WriteLine($"分类: {group.CategoryName}");
    foreach (var product in group.Products)
    {
        Console.WriteLine($"  - {product}");
    }
}

输出:

分类: 面包
  - 法棍
分类: 饮料
  - 百事可乐
  - 茶
分类: 肉类
  - 香肠

4. GroupJoin:查询语法(Query Syntax)

LINQ支持类似SQL的语法。group join用关键字join ... into ...,写法和上面差不多。


var groupJoin2 = from c in categories
                 join p in products on c.Id equals p.CategoryId into prodGroup
                 select new
                 {
                     CategoryName = c.Name,
                     Products = prodGroup.Select(p => p.Name).ToList()
                 };

这很像SQL里的LEFT OUTER JOIN ... GROUP BY

可视化:GroupJoin怎么工作的


[分类]         [商品]           分组(GroupJoin)

  面包   -------->   法棍          =>   面包:    [法棍]
  饮料   -------->   百事可乐      =>   饮料:    [百事可乐, 茶]
  饮料   -------->   茶
  肉类   -------->   香肠        =>   肉类:    [香肠]

每个分类会有自己的“小口袋”(IEnumerable<Product>),里面装着这个分类下的所有商品。

5. 特点和坑

GroupJoin vs. 普通Join

普通JoinGroupJoin的区别在于结果数量。Join每个匹配返回一对,GroupJoin每个外部元素返回一个,里面是所有匹配的集合。

如果有分类没有商品,用GroupJoin它还是会出现,只不过商品集合是空的。这和SQL里的LEFT OUTER JOIN(左外连接)一样。

比如有个没有商品的分类:


categories.Add(new Category { Id = 4, Name = "奶酪" });

var groupJoin3 = categories.GroupJoin(
    products,
    c => c.Id,
    p => p.CategoryId,
    (c, prods) => new
    {
        CategoryName = c.Name,
        Products = prods.Select(p => p.Name).ToList()
    });

foreach (var group in groupJoin3)
{
    Console.WriteLine($"分类: {group.CategoryName}");
    if (group.Products.Count == 0)
        Console.WriteLine("  (没有商品)");
    else
        foreach (var product in group.Products)
            Console.WriteLine($"  - {product}");
}
分类: 面包
  - 法棍
分类: 饮料
  - 百事可乐
  - 茶
分类: 肉类
  - 香肠
分类: 奶酪
  (没有商品)

这种场景在业务系统里很常见:要显示所有分类(或分组),即使有的分组里啥都没有。

GroupBy实现?别!

很多人会搞混,想用双GroupBy“模拟”GroupJoin。别这样——GroupJoin就是为这设计的,原生支持“左连接”。

6. 用真实数据

在前面讲的基础上,咱们给学习用的小程序加个报表功能:“每个分类下的商品列表”。这在电商、CRM、管理或报表系统里很常用。

加点代码到demo里:


// 假设已经有Category和Product类和集合

Console.WriteLine("分类和商品报表:");
var categoryReport = categories.GroupJoin(
    products,
    cat => cat.Id,
    prod => prod.CategoryId,
    (cat, prods) => new
    {
        cat.Name,
        ProductNames = prods.Select(p => p.Name).ToList()
    });

foreach (var row in categoryReport)
{
    Console.WriteLine($"分类: {row.Name}");
    if (row.ProductNames.Count == 0)
        Console.WriteLine("  (没有商品)");
    else
        foreach (var prodName in row.ProductNames)
            Console.WriteLine($"  - {prodName}");
}

7. 嵌套分组和聚合

GroupJoin可以和聚合函数一起用,做更复杂的报表。

例子:统计每个分类下商品数量


var reportWithCount = categories.GroupJoin(
    products,
    category => category.Id,
    product => product.CategoryId,
    (category, prods) => new
    {
        Category = category.Name,
        Count = prods.Count()   // 聚合函数!
    });

foreach (var rec in reportWithCount)
{
    Console.WriteLine($"{rec.Category}: {rec.Count}个商品");
}

假设categoriesproducts集合如下:


var categories = new[]
{
    new { Id = 1, Name = "水果" },
    new { Id = 2, Name = "蔬菜" },
    new { Id = 3, Name = "乳制品" }
};

var products = new[]
{
    new { Id = 1, Name = "苹果", CategoryId = 1 },
    new { Id = 2, Name = "香蕉", CategoryId = 1 },
    new { Id = 3, Name = "胡萝卜", CategoryId = 2 }
};

控制台输出:

分类: 水果 — 2个商品
分类: 蔬菜 — 1个商品
分类: 乳制品 — 0个商品

8. GroupJoin和新手常见错误

常见错误——以为GroupJoin会返回扁平表,像普通Join那样。这里的“扁平”指的是每行一对:一个外部元素和一个对应的内部元素(像SQL里INNER JOIN后的表)。

这对有点数据库经验的人尤其容易误导:他们以为GroupJoin会像LEFT JOIN那样,但其实它返回的是外部元素一组相关的内部元素——本质上是嵌套结构。

记得需要时“展开”嵌套集合——比如用SelectMany,如果你想要普通的配对序列。

还有个常见坑——忘了没匹配的元素子集合就是空列表。这是默认行为,不是bug——但要记住,不然会奇怪为啥输出“啥都没有”。

什么时候用GroupJoin,什么时候不用?

GroupJoin的场景:

  • 你有两组数据(比如部门和员工、分类和商品),要按层级显示:每个“父”下面所有“子”。
  • 做复杂报表,要显示所有主元素,即使“子元素”没有。
  • 需要SQLLEFT OUTER JOIN加按key分组的效果。

不要用GroupJoin在只需要交集或每个匹配只要一对的场景——那用普通Join就行。

评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION