【问题标题】:LINQ Left Outer Join Multiple Tables with Group Count and Row Concatenation具有组计数和行连接的 LINQ 左外连接多个表
【发布时间】:2021-10-29 22:49:31
【问题描述】:

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

表格

  • 父母(ParentId、ParentName、ParentOccupation)
  • 孩子(ChildId、ChildName、OtherField、ParentId)
  • GrandChild(GrandChildId、GrandChildName、OtherField、ChildId)

家长

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

儿童

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

孙子

+--------------+----------------+-------------+---------+
| 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 - 包含聚合但并非所有父列都存在。在计数中也返回 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 group-by left-join multi-table


    【解决方案1】:

    假设您使用的是 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();
    

    【讨论】:

    • 谢谢罗伯特。不幸的是,我收到此错误:“SqlException:无法对包含聚合或子查询的表达式执行聚合函数。”。
    • 有趣。您是否尝试过注释掉的版本?这是一个单一的聚合。
    • 正如我在编辑的答案中所说,罗伯特麦基的答案更相关。使用 EFCore 和 Sqlite 在 Linqpad 中运行这两个版本的查询也没有错误。
    • @RobertMcKee。由于某种原因,我昨晚都遇到了错误。但是我用评论部分再次尝试了它,它成功了!正如 Laurent 也指出的那样,我会将你的标记为答案。再次感谢您!
    • 已编辑答案以删除第一种方法,并取消注释第二种方法,以便它反映 OP 发现对他有用的内容。
    【解决方案2】:

    编辑

    问题作者发布的新 cmets 显示 Linq 查询涉及 EF Core。我最初的答案假设它是一个本地查询(Linq to Object)。事实上,它似乎是一个解释型查询(Linq to Entities)。

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

    在这种情况下,罗伯特·麦基的回答更中肯。

    出于好奇,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 对象

    这是构建查询的一种方式。

    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   
    

    【讨论】:

    • 我得到这个:“LINQ 表达式 'DbSet .Where(s => DbSet .Where(s0 => s0.ParentId == (Nullable)( EntityShaperExpression: EntityType: Parent ValueBufferExpression: (ProjectionBindingExpression: EmptyProjectionMember) IsNullable: False ).Id) .Select(s0 => s0.Id) .ToList() .Contains(s.ChildId ?? 0))' 无法翻译。要么以可以翻译的形式重写查询,要么通过插入对 AsEnumerable()、AsAsyncEnumerable()、ToList() 或 ToListAsync() 的调用显式切换到客户端评估。什么有效。
    • 该错误是由 .Net Core 3.1 不再支持的“包含”引起的。 (请参阅stackoverflow.com/questions/60412799/…)我所做的解决方案是在“var Result = Parents”之后添加“.ToList()”。谢谢你,劳伦特! LINQPad 脚本也有很大帮助。
    • 我根据您的 cmets 编辑了我的答案。 Robert McKee 的回答更中肯,因为您显然是在处理解释查询而不是本地查询。有关更多详细信息,请参阅我编辑的答案。理想情况下,您应该指定是使用 Linq to Objects 还是 Linq to Entities。您还可以使用 Linqpad 执行解释查询(这是一个很好的试验工具。它还允许检查生成的 SQL)。如果您不清楚 Linq to Objects 和 Linq to Entities 之间的区别,您应该研究一下。
    • 我一定会阅读您分享的链接,并注意其中的差异,以便将来如果我有类似的问题可以更好地指定它。感谢您的帮助!
    猜你喜欢
    • 2018-08-21
    • 2010-09-09
    • 2010-11-10
    • 2012-01-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多