2009-08-27 111 views
8

我想重寫一些舊的SQL到LINQ to SQL。我有一個使用GROUP BY WITH ROLLUP的存儲過程,但我不確定LINQ相當於什麼。 LINQ有一個GroupBy,但它看起來並不像它支持ROLLUP。LINQ到SQL版本的GROUP BY WITH ROLLUP

的結果,我試圖讓會是這樣的一個簡單的例子:

 
+-----------+---------------+--------------------+ 
| City | ServicePlan | NumberOfCustomers | 
+-----------+---------------+--------------------+ 
| Seattle | Plan A  |     10 | 
| Seattle | Plan B  |     5 | 
| Seattle | All   |     15 | 
| Portland | Plan A  |     20 | 
| Portland | Plan C  |     10 | 
| Portland | All   |     30 | 
| All  | All   |     45 | 
+-----------+---------------+--------------------+ 

關於我如何能使用LINQ to SQL獲得這些結果的任何想法?

回答

10

我想出了一個更簡單的解決方案。我試圖讓它變得比它需要的複雜。我只需要一種方法,而不需要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); 
    } 
} 
4

我明白了!一個通用的GroupByWithRollup。它只分成兩列,但可以很容易地擴展到支持更多。我可能會有另一個版本,接受三列。關鍵類別/方法分組爲<>,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); 
    } 
} 
+0

Bah,那裏還有bug。當我根據實際的SQL數據運行它時,它會拋出異常,因爲我需要使用表達式而不是僅僅使用Func <>。我也不能在表達式中使用「from x in y」語法。本文幫助:http://www.richardbushnell.net/index.php/2008/01/16/using-lambda-expressions-with-linq-to-sql/。所以我仍然需要清理乾淨。 – Ecyrb 2009-08-31 18:50:33

+0

這種方法比必要的要複雜得多。我無法完全在SQL方面進行分組。最後,我放棄了這種方法,並提出了更簡單的接受解決方案。 – Ecyrb 2009-09-09 19:39:15

2

@Ecyrb,你好從五年後!

我只是模糊地熟悉LINQ to SQL,超越標準LINQ(對象)。但是,由於您的「LINQ」標記與「LINQ-2-SQL」標記分開,因爲您似乎主要對結果感興趣(而不是註冊數據庫更改),並且因爲這是唯一的當我搜索LINQ相當於SQL Server的「Rollup」分組功能時,出現了一些真正的相關資源,我將爲今天有類似需求的任何人提供我自己的替代解決方案。基本上,我的方法是創建一個類似於「.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服務器的彙總邏輯。我更喜歡使用空值,因爲它是投射最靈活的常量。投射非常重要,因爲您在.GroupBy()和.ThenBy()中使用的函數必須導致相同的對象類型。使用您在08月31 '09你的第一反應創造了「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()」的邏輯,如你所願。

你可以在「.ThenBy()」的幫助下模擬SQL Server的分組集合,也可能模擬立方體。另外,「.ThenBy()」對我來說工作正常,而且我不擔心任何與「.OrderBy()」方法的「.ThenBy()」等效的問題,因爲它們具有不同的簽名,但如果出現問題,您可能需要考慮將其命名爲「.ThenGroupBy()」以區分。

如上所述,我不使用Linq-to-SQL,但是我使用F#的類型提供者系統,我知道在很多方面都使用Linq-to-SQL。所以我嘗試從我的F#項目中對這樣一個對象進行擴展,並且按照我的預期工作。雖然我完全不知道這是否意味着在這方面有什麼有趣的事情。