【问题标题】:LINQ to SQL version of GROUP BY WITH ROLLUPGROUP BY WITH ROLLUP 的 LINQ to SQL 版本
【发布时间】:2009-08-27 20:05:17
【问题描述】:

我正在尝试将一些旧 SQL 重写为 LINQ to SQL。我有一个带有 GROUP BY WITH ROLLUP 的存储过程,但我不确定 LINQ 等效项是什么。 LINQ 有一个 GroupBy,但它看起来不支持 ROLLUP。

我试图获得的结果的简化示例如下所示:

+-----------+----------------+--------------------+ |城市 |服务计划 |客户数量 | +-----------+----------------+--------------------+ |西雅图 |计划A | 10 | |西雅图 | B计划| 5 | |西雅图 |全部 | 15 | |波特兰 |计划A | 20 | |波特兰 |计划C | 10 | |波特兰 |全部 | 30 | |全部 |全部 | 45 | +-----------+----------------+--------------------+

关于如何使用 LINQ to SQL 获得这些结果的任何想法?

【问题讨论】:

    标签: linq linq-to-sql


    【解决方案1】:

    我想出了一个更简单的解决方案。我试图让它变得比它需要的更复杂。我只需要一种方法,而不是需要 3-5 个类/方法。

    基本上,您自己进行排序和分组,然后致电WithRollup() 以获取带有小计和总计的项目List<>。我不知道如何在 SQL 端生成小计和总计,所以这些都是使用 LINQ to Objects 完成的。代码如下:

    /// <summary>
    /// Adds sub-totals to a list of items, along with a grand total for the whole list.
    /// </summary>
    /// <param name="elements">Group and/or sort this yourself before calling WithRollup.</param>
    /// <param name="primaryKeyOfElement">Given a TElement, return the property that you want sub-totals for.</param>
    /// <param name="calculateSubTotalElement">Given a group of elements, return a TElement that represents the sub-total.</param>
    /// <param name="grandTotalElement">A TElement that represents the grand total.</param>
    public static List<TElement> WithRollup<TElement, TKey>(this IEnumerable<TElement> elements,
        Func<TElement, TKey> primaryKeyOfElement,
        Func<IGrouping<TKey, TElement>, TElement> calculateSubTotalElement,
        TElement grandTotalElement)
    {
        // Create a new list the items, subtotals, and the grand total.
        List<TElement> results = new List<TElement>();
        var lookup = elements.ToLookup(primaryKeyOfElement);
        foreach (var group in lookup)
        {
            // Add items in the current group
            results.AddRange(group);
            // Add subTotal for current group
            results.Add(calculateSubTotalElement(group));
        }
        // Add grand total
        results.Add(grandTotalElement);
    
        return results;
    }
    

    以及如何使用它的示例:

    class Program
    {
        static void Main(string[] args)
        {
            IQueryable<CustomObject> dataItems = (new[]
            {
                new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
                new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
                new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
                new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
                new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
                new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 }
            }).AsQueryable();
    
            IQueryable<CustomObject> orderedElements = from item in dataItems
                                                       orderby item.City, item.Plan
                                                       group item by new { item.City, item.Plan } into grouping
                                                       select new CustomObject
                                                       {
                                                           City = grouping.Key.City,
                                                           Plan = grouping.Key.Plan,
                                                           Charges = grouping.Sum(item => item.Charges),
                                                           Count = grouping.Count()
                                                       };
    
            List<CustomObject> results = orderedElements.WithRollup(
                item => item.City,
                group => new CustomObject
                {
                    City = group.Key,
                    Plan = "All",
                    Charges = group.Sum(item => item.Charges),
                    Count = group.Sum(item => item.Count)
                },
                new CustomObject
                {
                    City = "All",
                    Plan = "All",
                    Charges = orderedElements.Sum(item => item.Charges),
                    Count = orderedElements.Sum(item => item.Count)
                });
    
            foreach (var result in results)
                Console.WriteLine(result);
    
            Console.Read();
        }
    }
    
    class CustomObject
    {
        public string City { get; set; }
        public string Plan { get; set; }
        public int Count { get; set; }
        public decimal Charges { get; set; }
    
        public override string ToString()
        {
            return String.Format("{0} - {1} ({2} - {3})", City, Plan, Count, Charges);
        }
    }
    

    【讨论】:

      【解决方案2】:

      我明白了!一个通用的 GroupByWithRollup。它仅按两列分组,但可以轻松扩展以支持更多。我可能会有另一个接受三列的版本。关键类/方法是 Grouping、GroupByMany() 和 GroupByWithRollup()。当您实际使用 GroupByWithRollup() 时,SubTotal() 和 GrandTotal() 方法是助手。下面是代码,后面是如何使用的例子。

      /// <summary>
      /// Represents an instance of an IGrouping<>.  Used by GroupByMany(), GroupByWithRollup(), and GrandTotal().
      /// </summary>
      public class Grouping<TKey, TElement> : IGrouping<TKey, TElement>
      {
          public TKey Key { get; set; }
          public IEnumerable<TElement> Items { get; set; }
      
          public IEnumerator<TElement> GetEnumerator()
          {
              return Items.GetEnumerator();
          }
      
          IEnumerator IEnumerable.GetEnumerator()
          {
              return Items.GetEnumerator();
          }
      }
      
      public static class Extensions
      {
          /// <summary>
          /// Groups by two columns.
          /// </summary>
          /// <typeparam name="TElement">Type of elements to group.</typeparam>
          /// <typeparam name="TKey1">Type of the first expression to group by.</typeparam>
          /// <typeparam name="TKey2">Type of the second expression to group by.</typeparam>
          /// <param name="orderedElements">Elements to group.</param>
          /// <param name="groupByKey1Expression">The first expression to group by.</param>
          /// <param name="groupByKey2Expression">The second expression to group by.</param>
          /// <param name="newElementExpression">An expression that returns a new TElement.</param>
          public static IQueryable<Grouping<TKey1, TElement>> GroupByMany<TElement, TKey1, TKey2>(this IOrderedQueryable<TElement> orderedElements,
              Func<TElement, TKey1> groupByKey1Expression,
              Func<TElement, TKey2> groupByKey2Expression,
              Func<IGrouping<TKey1, TElement>, IGrouping<TKey2, TElement>, TElement> newElementExpression
              )
          {
              // Group the items by Key1 and Key2
              return from element in orderedElements
                     group element by groupByKey1Expression(element) into groupByKey1
                     select new Grouping<TKey1, TElement>
                     {
                         Key = groupByKey1.Key,
                         Items = from key1Item in groupByKey1
                                 group key1Item by groupByKey2Expression(key1Item) into groupByKey2
                                 select newElementExpression(groupByKey1, groupByKey2)
                     };
          }
      
          /// <summary>
          /// Returns a List of TElement containing all elements of orderedElements as well as subTotals and a grand total.
          /// </summary>
          /// <typeparam name="TElement">Type of elements to group.</typeparam>
          /// <typeparam name="TKey1">Type of the first expression to group by.</typeparam>
          /// <typeparam name="TKey2">Type of the second expression to group by.</typeparam>
          /// <param name="orderedElements">Elements to group.</param>
          /// <param name="groupByKey1Expression">The first expression to group by.</param>
          /// <param name="groupByKey2Expression">The second expression to group by.</param>
          /// <param name="newElementExpression">An expression that returns a new TElement.</param>
          /// <param name="subTotalExpression">An expression that returns a new TElement that represents a subTotal.</param>
          /// <param name="totalExpression">An expression that returns a new TElement that represents a grand total.</param>
          public static List<TElement> GroupByWithRollup<TElement, TKey1, TKey2>(this IOrderedQueryable<TElement> orderedElements,
              Func<TElement, TKey1> groupByKey1Expression,
              Func<TElement, TKey2> groupByKey2Expression,
              Func<IGrouping<TKey1, TElement>, IGrouping<TKey2, TElement>, TElement> newElementExpression,
              Func<IGrouping<TKey1, TElement>, TElement> subTotalExpression,
              Func<IQueryable<Grouping<TKey1, TElement>>, TElement> totalExpression
              )
          {
              // Group the items by Key1 and Key2
              IQueryable<Grouping<TKey1, TElement>> groupedItems = orderedElements.GroupByMany(groupByKey1Expression, groupByKey2Expression, newElementExpression);
      
              // Create a new list the items, subtotals, and the grand total.
              List<TElement> results = new List<TElement>();
              foreach (Grouping<TKey1, TElement> item in groupedItems)
              {
                  // Add items under current group
                  results.AddRange(item);
                  // Add subTotal for current group
                  results.Add(subTotalExpression(item));
              }
              // Add grand total
              results.Add(totalExpression(groupedItems));
      
              return results;
          }
      
          /// <summary>
          /// Returns the subTotal sum of sumExpression.
          /// </summary>
          /// <param name="sumExpression">An expression that returns the value to sum.</param>
          public static int SubTotal<TKey, TElement>(this IGrouping<TKey, TElement> query, Func<TElement, int> sumExpression)
          {
              return query.Sum(group => sumExpression(group));
          }
      
          /// <summary>
          /// Returns the subTotal sum of sumExpression.
          /// </summary>
          /// <param name="sumExpression">An expression that returns the value to sum.</param>
          public static decimal SubTotal<TKey, TElement>(this IGrouping<TKey, TElement> query, Func<TElement, decimal> sumExpression)
          {
              return query.Sum(group => sumExpression(group));
          }
      
          /// <summary>
          /// Returns the grand total sum of sumExpression.
          /// </summary>
          /// <param name="sumExpression">An expression that returns the value to sum.</param>
          public static int GrandTotal<TKey, TElement>(this IQueryable<Grouping<TKey, TElement>> query, Func<TElement, int> sumExpression)
          {
              return query.Sum(group => group.Sum(innerGroup => sumExpression(innerGroup)));
          }
      
          /// <summary>
          /// Returns the grand total sum of sumExpression.
          /// </summary>
          /// <param name="sumExpression">An expression that returns the value to sum.</param>
          public static decimal GrandTotal<TKey, TElement>(this IQueryable<Grouping<TKey, TElement>> query, Func<TElement, decimal> sumExpression)
          {
              return query.Sum(group => group.Sum(innerGroup => sumExpression(innerGroup)));
          }
      

      还有一个使用它的例子:

      class Program
      {
          static void Main(string[] args)
          {
              IQueryable<CustomObject> dataItems = (new[]
              {
                  new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
                  new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Seattle", Plan = "Plan B", Charges = 20 },
                  new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Seattle", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Portland", Plan = "Plan A", Charges = 10 },
                  new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
                  new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 },
                  new CustomObject { City = "Portland", Plan = "Plan C", Charges = 30 }
              }).AsQueryable();
      
              List<CustomObject> results = dataItems.OrderBy(item => item.City).ThenBy(item => item.Plan).GroupByWithRollup(
                  item => item.City,
                  item => item.Plan,
                  (primaryGrouping, secondaryGrouping) => new CustomObject
                  {
                      City = primaryGrouping.Key,
                      Plan = secondaryGrouping.Key,
                      Count = secondaryGrouping.Count(),
                      Charges = secondaryGrouping.Sum(item => item.Charges)
                  },
                  item => new CustomObject
                  {
                      City = item.Key,
                      Plan = "All",
                      Count = item.SubTotal(subItem => subItem.Count),
                      Charges = item.SubTotal(subItem => subItem.Charges)
                  },
                  items => new CustomObject
                  {
                      City = "All",
                      Plan = "All",
                      Count = items.GrandTotal(subItem => subItem.Count),
                      Charges = items.GrandTotal(subItem => subItem.Charges)
                  }
                  );
              foreach (var result in results)
                  Console.WriteLine(result);
      
              Console.Read();
          }
      }
      
      class CustomObject
      {
          public string City { get; set; }
          public string Plan { get; set; }
          public int Count { get; set; }
          public decimal Charges { get; set; }
      
          public override string ToString()
          {
              return String.Format("{0} - {1} ({2} - {3})", City, Plan, Count, Charges);
          }
      }
      

      【讨论】:

      • 呸,里面还有bug。当我针对实际 SQL 数据运行它时,它会引发异常,因为我需要使用 Expression> 而不仅仅是 Func。我也不能在 Expressions 中使用“from x in y”语法。这篇文章对此有所帮助:richardbushnell.net/index.php/2008/01/16/…。所以我仍然需要把它清理干净。
      • 这种方法比必要的复杂得多。我无法让分组完全在 SQL 端工作。最后,我放弃了这种方法,并提出了更简单的公认解决方案。
      【解决方案3】:

      @Ecyrb,五年后你好!

      我对标准 LINQ(对象)之外的 LINQ to SQL 只是模糊地熟悉。但是,由于您确实有一个与“LINQ-2-SQL”标签分开的“LINQ”标签,因为您似乎主要对结果感兴趣(而不是向数据库注册更改),并且因为这是唯一的当我在谷歌上搜索与 SQL Server 的“汇总”分组功能等效的 LINQ 时出现的真正相关资源,我将为今天有类似需求的任何人提供我自己的替代解决方案。

      基本上,我的方法是创建类似于“.OrderBy().ThenBy()”语法的“.GroupBy().ThenBy()”可链接语法。我的扩展需要一个 IGrouping 对象的集合——你从运行“.GroupBy()”得到的结果——作为它的源。然后,它获取集合并将它们取消分组,以便在分组之前返回原始对象。最后根据新的分组函数对数据进行重新分组,生成另一组IGrouping对象,并将新分组的对象加入到源对象集合中。

      public static class mySampleExtensions {
      
          public static IEnumerable<IGrouping<TKey, TSource>> ThenBy<TSource, TKey> (     
              this IEnumerable<IGrouping<TKey, TSource>> source,
              Func<TSource, TKey> keySelector) {
      
              var unGroup = source.SelectMany(sm=> sm).Distinct(); // thank you flq at http://stackoverflow.com/questions/462879/convert-listlistt-into-listt-in-c-sharp
              var reGroup = unGroup.GroupBy(keySelector);
      
              return source.Concat(reGroup);}
      
      }
      

      您可以使用该方法通过将常量值放入“.ThenBy()”函数的适当区域来匹配 SQL Server 的汇总逻辑。我更喜欢使用 null 值,因为它是最灵活的转换常量。强制转换很重要,因为您在 .GroupBy() 和 .ThenBy() 中使用的函数必须产生相同的对象类型。使用您在 2009 年 8 月 31 日的第一个响应中创建的“dataItems”变量,它看起来像这样:

      var rollItUp = dataItems
          .GroupBy(g=> new {g.City, g.Plan})
              .ThenBy(g=> new {g.City, Plan = (string) null})
              .ThenBy(g=> new {City = (string) null, Plan = (string) null})
          .Select(s=> new CustomObject {
              City = s.Key.City, 
              Plan = s.Key.Plan, 
              Count = s.Count(),
              Charges = s.Sum(a=> a.Charges)})
          .OrderBy(o=> o.City) // This line optional
              .ThenBy(o=> o.Plan); // This line optional
      

      您可以根据需要将“.ThenBy()”逻辑中的空值替换为“all”。

      在“.ThenBy()”的帮助下,您可以潜在地模拟 SQL Server 的分组集,也许还有多维数据集。此外,“.ThenBy()”对我来说工作正常,我不认为名称等同于“.OrderBy()”方法的“.ThenBy()”有任何问题,因为它们有不同的签名,但如果有问题,不妨考虑将其命名为“.ThenGroupBy()”来区分。

      如前所述,我不使用 Linq-to-SQL,但我使用 F# 的类型提供程序系统,据我了解,它在许多方面使用 Linq-to-SQL。所以我在我的 F# 项目中尝试了对这样一个对象的扩展,它按我的预期工作。虽然我完全不知道这在这方面是否意味着任何有趣的事情。

      【讨论】:

        【解决方案4】:

        这里提供了非常有趣的解决方案

        https://blogs.msdn.microsoft.com/mitsu/2007/12/21/playing-with-linq-grouping-groupbymany/

        它描述了如何通过几个属性来执行 groupbby。即:

        var result = customers.GroupByMany(c => c.Country, c => c.City);
        

        因此,您将获得可以简单地转换为平面列表的层次结构。

        【讨论】:

          猜你喜欢
          • 2010-12-31
          • 2017-05-29
          • 1970-01-01
          • 2010-12-13
          • 1970-01-01
          • 2010-11-15
          • 1970-01-01
          • 2015-04-26
          • 1970-01-01
          相关资源
          最近更新 更多