2009-08-27 12 views
8

Próbuję przepisać niektóre stare SQL na LINQ na SQL. Mam sproc z GROUP BY WITH ROLLUP, ale nie jestem pewien, jaki byłby odpowiednik LINQ. LINQ ma GroupBy, ale nie wygląda na to, że obsługuje ROLLUP.LINQ do wersji SQL GROUP BY WITH ROLLUP

Uproszczony przykład wyników Próbuję dostać byłoby coś takiego:

 
+-----------+---------------+--------------------+ 
| 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 | 
+-----------+---------------+--------------------+ 

pomysłów, w jaki sposób mogę uzyskać te wyniki przy użyciu LINQ do SQL?

Odpowiedz

10

zorientowali się znacznie prostsze rozwiązanie. Próbowałem uczynić go bardziej skomplikowanym, niż powinien. Zamiast potrzebować 3-5 klas/metod, potrzebuję tylko jednej metody.

Zasadniczo sam sortujesz i grupujesz, a następnie dzwonisz pod numer WithRollup(), aby uzyskać List<> produktów z sumami częściowymi i sumą całkowitą. Nie mogłem wymyślić, jak wygenerować podsumy i sumę całkowitą po stronie SQL, więc te są wykonywane z LINQ do obiektów. Oto kod:

/// <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; 
} 

a przykładem tego, jak z niego korzystać:

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

Mam to! Ogólny GroupByWithRollup. Grupuje tylko według dwóch kolumn, ale można go łatwo rozszerzyć, aby obsługiwać więcej. Prawdopodobnie będę mieć inną wersję, która akceptuje trzy kolumny. Najważniejsze klasy/metody to: Grupowanie <>, GroupByMany <>() i GroupByWithRollup <>(). Metody SubTotal() i GrandTotal() są pomocnikami, gdy faktycznie używasz GroupByWithRollup <>(). Poniżej znajduje się kod, a następnie przykład użycia go.

/// <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))); 
    } 

i przykład użycia go:

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, wciąż tam są robaki. Kiedy uruchomię go względem rzeczywistych danych SQL, generuje wyjątki, ponieważ muszę używać Expression > zamiast tylko Func <>. Nie mogę również używać składni "od x w y" w wyrażeniach. Ten artykuł pomógł w tym: http://www.richardbushnell.net/index.php/2008/01/16/using-lambda-expressions-with-linq-to-sql/. Więc nadal muszę to wyczyścić. – Ecyrb

+0

Podejście to okazało się o wiele bardziej złożone niż to konieczne. Nie udało mi się przekonać grupy do pracy po stronie SQL. W końcu porzuciłem to podejście i zaproponowałem znacznie prostsze akceptowane rozwiązanie. – Ecyrb

2

@Ecyrb, cześć z pięć lat później!

Jestem tylko mało znany z LINQ do SQL powyżej i poza standardowy LINQ (do obiektów). Ponieważ jednak znacznik "LINQ" jest oddzielony od znacznika "LINQ-2-SQL", ponieważ wydaje się, że interesujesz się głównie wynikami (w przeciwieństwie do rejestrowania zmian w bazie danych), a ponieważ jest to jedyne Prawdziwy trafny zasób, który pojawił się podczas wyszukiwania w Google w poszukiwaniu równoważnika LINQ funkcji grupującej "Rollup" programu SQL Server, zaproponuję własne alternatywne rozwiązanie dla każdego, kto ma podobne potrzeby.

Zasadniczo, moim podejściem jest stworzenie "łańcuchowej składni" .GroupBy(). ThenBy() "podobnej do składni" .OrderBy(). ThenBy() ". Moje rozszerzenie oczekuje kolekcji obiektów IGrouping - wynik uzyskany z uruchomienia ".GroupBy()" - jako jego źródło. Następnie zbiera i rozdziela je, aby wrócić do oryginalnego obiektu przed zgrupowaniem. Na koniec ponownie grupuje dane zgodnie z nową funkcją grupowania, tworząc kolejny zestaw obiektów IGrouping i dodaje nowo zgrupowane obiekty do zestawu obiektów źródłowych.

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);} 

} 

Można użyć metody pasujące logiki zbiorczy serwera SQL poprzez wprowadzenie wartości stałych do odpowiedniego obszaru „.ThenBy()” funkcji. Wolę używać wartości pustej, ponieważ jest to najbardziej elastyczna stała dla rzutowania. Przesyłanie jest ważne, ponieważ funkcja używana zarówno w .GroupBy() i .ThenBy() musi dawać ten sam typ obiektu.Korzystanie z „dataItems” zmienną utworzoną w swojej pierwszej odpowiedzi w dniu sie 31 '09, to będzie wyglądać następująco:

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 

Można zastąpić wartości null w „.ThenBy()” logiki z «all» jak chcesz.

Można potencjalnie emulować zestawów grupowania SQL Server, a może kostki, za pomocą ".ThenBy()". Również ".ThenBy()" działa dobrze dla mnie i nie oczekuję żadnych problemów z nazwą odpowiadającą ".ThenBy()" metody ".OrderBy()", ponieważ mają one różne sygnatury, ale jeśli występują problemy, możesz rozważyć nadanie jej nazwy ".ThenGroupBy()", aby je rozróżnić.

Jak już wspomniano, nie używam Linq-SQL, ale używam systemu dostawcy typu F #, który, jak rozumiem, pod wieloma względami korzysta z Linq-to-SQL pod maską. Więc spróbowałem mojego rozszerzenia na taki obiekt z mojego projektu F # i działa on tak, jak się spodziewałem. Chociaż nie mam absolutnie żadnego pojęcia, czy oznacza to coś interesującego, czy nie w tym względzie.