2009-08-27 22 views
8

Estoy intentando reescribir algunos SQL antiguos en LINQ to SQL. Tengo un sproc con GROUP BY WITH ROLLUP pero no estoy seguro de cuál sería el equivalente LINQ. LINQ tiene un GroupBy pero no parece que sea compatible con ROLLUP.LINQ to SQL versión de GROUP BY WITH ROLLUP

Un ejemplo simplificado de los resultados que estoy tratando de conseguir sería algo como esto:

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

Cualquier ideas sobre cómo podría conseguir estos resultados utilizando LINQ to SQL?

Respuesta

10

me di cuenta de una solución mucho más simple. Intentaba hacerlo mucho más complicado de lo necesario. En lugar de necesitar 3-5 clases/métodos, solo necesito un método.

Básicamente, usted hace su clasificación y agrupación usted mismo y luego llama al WithRollup() para obtener un List<> de los artículos con subtotales y un total general. No pude descifrar cómo generar los subtotales y el total general en el lado SQL, por lo que se hacen con LINQ to Objects. Aquí está el código:

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

Y un ejemplo de cómo se usa:

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

¡Lo tengo! Un GroupByWithRollup genérico. Solo agrupa por dos columnas, pero podría ampliarse fácilmente para admitir más. Probablemente tendré otra versión que acepte tres columnas. Las clases/métodos clave son Agrupamiento <>, GroupByMany <>() y GroupByWithRollup <>(). Los métodos SubTotal() y GrandTotal() son auxiliares cuando realmente usa GroupByWithRollup <>(). A continuación se muestra el código, seguido de un ejemplo de cómo usarlo.

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

Y un ejemplo de su uso:

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, todavía hay errores allí. Cuando lo ejecuto contra datos SQL reales arroja excepciones porque necesito usar Expression > en lugar de simplemente Func <>. Tampoco puedo usar la sintaxis "from x in y" con Expressions. Este artículo ayudó con eso: http://www.richardbushnell.net/index.php/2008/01/16/using-lambda-expressions-with-linq-to-sql/. Así que todavía tengo que limpiar eso. – Ecyrb

+0

Este enfoque resultó ser mucho más complejo de lo necesario. No pude hacer que la agrupación funcione por completo en el lado de SQL. Al final, abandoné este enfoque y se me ocurrió la solución aceptada mucho más simple. – Ecyrb

2

@Ecyrb, hola de cinco años más tarde!

Estoy solo vagamente familiarizado con LINQ to SQL por encima y más allá de LINQ estándar (a los objetos). Sin embargo, dado que tiene una etiqueta "LINQ" separada de su etiqueta "LINQ-2-SQL", porque parece estar interesado principalmente en los resultados (en oposición a registrar cambios con la base de datos), y porque este es el único verdadero recurso relevante que surgió cuando busqué en Google en busca de un LINQ equivalente de la función de agrupamiento "Rollup" de SQL Server, ofreceré mi propia solución alternativa para cualquier persona hoy con una necesidad similar.

Básicamente, mi enfoque es crear una sintaxis ".GroupBy(). ThenBy()" similar a la sintaxis ".OrderBy(). ThenBy()". Mi extensión espera una colección de objetos IGrouping: el resultado que obtienes al ejecutar ".GroupBy()" como fuente. A continuación, toma la recopilación y los desagrupa para volver al objeto original antes de agruparlos. Finalmente, vuelve a agrupar los datos según la nueva función de agrupación, produciendo otro conjunto de objetos IGrouping y agrega los objetos recién agrupados al conjunto de objetos de origen.

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

} 

Usted puede utilizar el método para que coincida con la lógica de resumen del servidor SQL poniendo valores constantes en el área apropiada de la ".ThenBy()" función. Prefiero usar el valor nulo porque es la constante más flexible para el casting. La conversión es importante porque la función que utiliza en .GroupBy() y .ThenBy() debe dar como resultado el mismo tipo de objeto.Usando el "dataItems" variable que ha creado en su primera respuesta en 31 ago '09, que se vería así:

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 

podría reemplazar los valores nulos en los ".ThenBy) (" lógicas 'con todo', Como desees.

Puede emular los conjuntos de agrupación de SQL Server, y quizás el cubo, con la ayuda de ".ThenBy()". Además, ".ThenBy()" funciona bien para mí, y no preveo que el nombre sea equivalente al ".ThenBy()" del método ".OrderBy()" ya que tienen firmas diferentes, pero si hay problemas, puede considerar nombrarlos ".ThenGroupBy()" para distinguir.

Como mencioné, no uso Linq-to-SQL, pero utilizo el sistema de proveedores de tipo F #, que entiendo utiliza Linq-to-SQL bajo el capó en muchos aspectos. Así que probé mi extensión en un objeto de este tipo desde mi proyecto F #, y funciona como esperaba. Aunque no tengo ni idea de si esto significa algo interesante o no en este sentido. Se proporciona