LINQ 左外连接多个表与组计数和行串联

LINQ Left Outer Join Multiple Tables with Group Count and Row Concatenation

有人可以帮忙解决以下问题吗?我简化了 table/column 名称等。我到处搜索,但我得到的答案是我想要在下面实现的结果的不完整解决方案。 LINQ 的新手,所以请善待。 :-)

表格

Parent

+----------+------------+------------------+
| ParentId | ParentName | ParentOccupation |
+----------+------------+------------------+
| 1        | Mary       | Teacher          |
| 2        | Anne       | Doctor           |
| 3        | Michael    | Farmer           |
| 4        | Elizabeth  | Police           |
| 5        | Andrew     | Fireman          |
+----------+------------+------------------+

Child

+---------+-----------+-------------+----------+
| ChildId | ChildName | OtherField  | ParentId |
+---------+-----------+-------------+----------+
| 1       | Ashley    | [SomeValue] | 1        |
| 2       | Brooke    | [SomeValue] | 1        |
| 3       | Ashton    | [SomeValue] | 3        |
| 4       | Emma      | [SomeValue] | 4        |
+---------+-----------+-------------+----------+

盛大Child

+--------------+----------------+-------------+---------+
| GrandChildId | GrandChildName | OtherField  | ChildId |
+--------------+----------------+-------------+---------+
| 1            | Andrew         | [SomeValue] | 1       |
| 2            | Isabelle       | [SomeValue] | 2       |
| 3            | Lucas          | [SomeValue] | 2       |
| 4            | Matthew        | [SomeValue] | 4       |
+--------------+----------------+-------------+---------+

预期结果

+----------+------------+------------------+-----------------------+-------------------------+
| ParentId | ParentName | ParentOccupation | NumberOfGrandChildren | NamesOfGrandChildren    |
+----------+------------+------------------+-----------------------+-------------------------+
| 1        | Mary       | Teacher          | 3                     | Andrew, Isabelle, Lucas |
| 2        | Anne       | Doctor           | 0                     |                         |   
| 3        | Michael    | Farmer           | 0                     |                         |
| 4        | Elizabeth  | Police           | 1                     | Matthew                 |
| 5        | Andrew     | Fireman          | 0                     |                         | 
+----------+------------+------------------+-----------------------+-------------------------+

到目前为止我做了什么

LEFT OUTER JOINS - 获取所有列但没有聚合

var result1 = (from p in Parent
               join c in Child on p.ParentId equals c.ParentId into pcj
               from pc in pcj.DefaultIfEmpty()
               join g in GrandChild on pc.ChildId equals g.ChildId into cgj
               from cg in cgj.DefaultIfEmpty()
               where [some criteria]
               select new 
               {
                  ParentId = p.ParentId,
                  ParentName = p.ParentName,
                  ChildId = pc.ChildId,
                  ChildName = pc.ChildName,
                  GrandChildId = cg.GrandChildId,
                  GrandChildName = cg.GrandChildName   
               });

COUNTS - 包含聚合但并非所有 parent 列都在那里。另外 returns 计数为 1,而不是 0。

var result2 = (from p in Parent
               join c in Child on p.ParentId equals c.ParentId into pcj
               from pc in pcj.DefaultIfEmpty()
               join g in GrandChild on pc.ChildId equals g.ChildId into cgj
               from cg in cgj.DefaultIfEmpty()
               where [some criteria]
               group new { p } by new { p.ParentId } into r
               select new 
               {
                  ParentId = r.Key.Id,
                  NumberOfGrandChildren = r.Count()
               });

CONCATENATE COMMA SEPARATED ROW VALUES(用于孙辈的名字)- 在我解决上面的问题之前还没有尝试过,但请打开解决方案。

如何结合并实现上述结果?任何帮助表示赞赏!提前致谢。

编辑

题主发表的新评论表明Linq查询涉及EF Core。我最初的回答假设这是一个本地查询(Linq to Object)。事实上,它更像是一个解释查询(Linq to Entities)。

有关 Linq to object 和 Linq to entities 之间区别的解释,请参阅 linq to entities vs linq to objects - are they the same?

既然如此,Robert McKee 的回答就更切题了。

出于好奇,Linqpad 显示此查询:

Parents
    .Select(p => new
    {
        ParentId = p.Id,
        ParentName = p.Name,
        ParentOccupation = p.Occupation,
        GrandChildrenCount = p.Children
            .SelectMany(c => c.GrandChildren)
            .Count(),
        GranchildrenNames = string.Join(", ", p.Children
            .SelectMany(c => c.GrandChildren)
            .Select(gc => gc.Name))
    });

将翻译成以下 SQL 查询:

SELECT "p"."Id", "p"."Name", "p"."Occupation", (
    SELECT COUNT(*)
    FROM "Children" AS "c"
    INNER JOIN "GrandChildren" AS "g" ON "c"."Id" = "g"."ChildId"
    WHERE "p"."Id" = "c"."ParentId"), "t"."Name", "t"."Id", "t"."Id0"
FROM "Parents" AS "p"
LEFT JOIN (
    SELECT "g0"."Name", "c0"."Id", "g0"."Id" AS "Id0", "c0"."ParentId"
    FROM "Children" AS "c0"
    INNER JOIN "GrandChildren" AS "g0" ON "c0"."Id" = "g0"."ChildId"
) AS "t" ON "p"."Id" = "t"."ParentId"
ORDER BY "p"."Id", "t"."Id", "t"."Id0"

(使用 Sqlite,以及包含具有导航属性的实体 类 的自定义 EFCore 上下文)



原始答案 - 假设 Linq to object

您可以通过以下方式构建查询。

var Result = Parents
    // Stage 1: for each parent, get its Chidren Ids
    .Select(p => new
    {
        Parent = p,
        ChildrenIds = Children
            .Where(c => c.ParentId == p.Id)
            .Select(c => c.Id)
            .ToList()
    })
    // Stage 2: for each parent, get its Grandchildren, by using the childrenIds list constructed before
    .Select(p => new
    {
        p.Parent,
        GrandChildren = Grandchildren
            .Where(gc => p.ChildrenIds.Contains(gc.ChildId))
            .ToList()
    })
    // Stage 3: for each parent, count the grandchildren, and get their names
    .Select(p => new
    {
        
        ParentId = p.Parent.Id,
        ParentName = p.Parent.Name,
        ParentOccupation = p.Parent.Occupation,
        NumberOfGrandChildren = p.GrandChildren.Count(),
        GranchildrenNames = string.Join(", ", p.GrandChildren.Select(gc => gc.Name))
    });

这是一个完整的 LinqPad 脚本,可以随机生成数据,所以你可以试试看:

void Main()
{
    var rnd = new Random();
    var Parents = Enumerable
        .Range(0, 10)
        .Select(i => new Parent
        {
            Id = i,
            Name = $"Parent-{i}",
            Occupation = $"Occupation{i}"
        })
        .ToList();
    var Children = Enumerable
        .Range(0,15)
        .Select(i => new Child
        {
            Id = i,
            Name = $"Child{i}",
            ParentId = rnd.Next(0, 10)
        })
        .ToList();
    var GrandChildren = Enumerable
        .Range(0, 25)
        .Select(i => new GrandChildren
        {
            Id = i,
            Name = $"GrandChild{i}",
            ChildId = rnd.Next(0, 15)
        })
        .ToList();


    var Result = Parents
        // Stage 1: for each parent, get its Chidren Ids
        .Select(p => new
        {
            Parent = p,
            ChildrenIds = Children
                .Where(c => c.ParentId == p.Id)
                .Select(c => c.Id)
                .ToList()
        })
        // Stage 2: for each parent, get its Grandchildren, by using the childrenIds list constructed before
        .Select(p => new
        {
            p.Parent,
            GrandChildren = GrandChildren
                .Where(gc => p.ChildrenIds.Contains(gc.ChildId))
                .ToList()
        })
        // Stage 3: for each parent, count the grandchildren, and get their names
        .Select(p => new
        {
            
            ParentId = p.Parent.Id,
            ParentName = p.Parent.Name,
            ParentOccupation = p.Parent.Occupation,
            NumberOfGrandChildren = p.GrandChildren.Count(),
            GranchildrenNames = string.Join(", ", p.GrandChildren.Select(gc => gc.Name))
        })
        .Dump();
}

// You can define other methods, fields, classes and namespaces here
public class Parent
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string Occupation { get; set; }
}

public class Child
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int ParentId { get; set; }
}

public class GrandChildren
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int ChildId { get; set; }
}

这是一组结果:

// Parents
0   Parent-0    Occupation0
1   Parent-1    Occupation1
2   Parent-2    Occupation2
3   Parent-3    Occupation3
4   Parent-4    Occupation4
5   Parent-5    Occupation5
6   Parent-6    Occupation6
7   Parent-7    Occupation7
8   Parent-8    Occupation8
9   Parent-9    Occupation9
// Children
0   Child0  1
1   Child1  5
2   Child2  8
3   Child3  6
4   Child4  9
5   Child5  3
6   Child6  0
7   Child7  4
8   Child8  9
9   Child9  7
10  Child10 8
11  Child11 2
12  Child12 7
13  Child13 7
14  Child14 8
// GrandChildren
0   GrandChild0 7
1   GrandChild1 11
2   GrandChild2 11
3   GrandChild3 14
4   GrandChild4 6
5   GrandChild5 0
6   GrandChild6 11
7   GrandChild7 6
8   GrandChild8 0
9   GrandChild9 12
10  GrandChild10    9
11  GrandChild11    7
12  GrandChild12    0
13  GrandChild13    3
14  GrandChild14    11
15  GrandChild15    9
16  GrandChild16    2
17  GrandChild17    12
18  GrandChild18    12
19  GrandChild19    12
20  GrandChild20    14
21  GrandChild21    12
22  GrandChild22    11
23  GrandChild23    14
24  GrandChild24    12
// Result
0   Parent-0    Occupation0 2   GrandChild4, GrandChild7
1   Parent-1    Occupation1 3   GrandChild5, GrandChild8, GrandChild12
2   Parent-2    Occupation2 5   GrandChild1, GrandChild2, GrandChild6, GrandChild14, GrandChild22
3   Parent-3    Occupation3 0   
4   Parent-4    Occupation4 2   GrandChild0, GrandChild11
5   Parent-5    Occupation5 0   
6   Parent-6    Occupation6 1   GrandChild13
7   Parent-7    Occupation7 8   GrandChild9, GrandChild10, GrandChild15, GrandChild17, GrandChild18, GrandChild19, GrandChild21, GrandChild24
8   Parent-8    Occupation8 4   GrandChild3, GrandChild16, GrandChild20, GrandChild23
9   Parent-9    Occupation9 0   

假设您使用的是 EF,并且设置了导航属性,那么您的查询将如下所示:

var result = context.Parents
  .Select(p => new {
    p.ParentId,
    p.ParentName,
    p.ParentOccupation,
    NumberOfGrandChildren = p.Children
       .SelectMany(c => c.GrandChildren)
       .Count(),
    NamesOfGrandChildren = string.Join(", ", p.Children
      .SelectMany(c => c.GrandChildren)
      .Select(g => g.GrandChildName))
  }).ToList();