2010-10-14 21 views
10

Necesito traducir la siguiente consulta LINQ a Dynamic LINQ que acepta varias columnas de agrupamiento según la entrada del usuario. Básicamente tengo un montón de listas desplegables que aplican agrupaciones y no quiero enumerar todas las combinaciones de agrupaciones. Si Dynamic LINQ falla, puedo tener que construir una consulta SQL manualmente, y nadie quiere eso.Dynamic LINQ GroupBy Columnas múltiples

var grouping = (from entry in ObjectContext.OmniturePageModules 
    where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && 
     (section == "Total" || section == "All" || entry.Section == section) && 
     (page == "Total" || page == "All" || entry.Page == page) && 
     (module == "Total" || module == "All" || entry.Module == module) 
    group entry by new 
    { 
     entry.Page, // I want to be able to tell this anonymous type 
     entry.Module, // which columns to group by 
     entry.StartOfWeek // at runtime 
    } 
    into entryGroup 
    select new 
    { 
     SeriesName = section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module, 
     Week = entryGroup.Key.StartOfWeek, 
     Clicks = entryGroup.Sum(p => p.Clicks) 
    }); 

no tengo idea de cómo hacer esto tan dinámico LINQ está totalmente indocumentado fuera de la "Hello World!" select/where/orderby casos. Simplemente no puedo entender la sintaxis.

Algo así como :(?)

var grouping = ObjectContext.OmniturePageModules.Where(entry => entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && 
              (section == "Total" || section == "All" || entry.Section == section) && 
              (page == "Total" || page == "All" || entry.Page == page) && 
              (module == "Total" || module == "All" || entry.Module == module)) 
              .GroupBy("new (StartOfWeek,Page,Module)", "it") 
              .Select("new (Sum(Clicks) as Clicks, SeriesName = section + key.Page + Key.Module, Week = it.Key.StartOfWeek)"); 

estoy usando la clase DynamicQueryable en System.Linq.Dynamic. Ver: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Seguimiento: solución de Enigmativity trabajó sobre todo . Por alguna razón, no quiere por el grupo de fecha y hora "StartOfWeek" columna - solución es sólo para hacer una agrupación secundaria:

var entries = (from entry in ObjectContext.OmniturePageModules 
          where entry.StartOfWeek >= startDate 
           && entry.StartOfWeek <= endDate 
           && (section == "Total" || section == "All" || entry.Section == section) 
           && (page == "Total" || page == "All" || entry.Page == page) 
           && (module == "Total" || module == "All" || entry.Module == module) 
          select entry).ToArray(); // Force query execution 

      var grouping = from entry in entries 
          let grouper = new EntryGrouper(entry, section, page, module) 
          group entry by grouper into entryGroup 
          select new 
          { 
           entryGroup.Key.SeriesName, 
           entryGroup.Key.Date, 
           Clicks = entryGroup.Sum(p => p.Clicks), 
          }; 

      var grouping2 = (from groups in grouping 
          group groups by new {groups.SeriesName, groups.Date } into entryGroup 
          select new 
          { 
           entryGroup.Key.SeriesName, 
           entryGroup.Key.Date, 
           Clicks = entryGroup.Sum(p => p.Clicks), 
          }); 

pero esto parece degradar seriamente el rendimiento ... =/

Respuesta

3

Si desea utilizar explícitamente la Biblioteca de consultas dinámicas LINQ, mi respuesta no será la que desea, pero si quiere el comportamiento deseado y está contento de utilizar el LINQ regular, entonces creo que puedo ayudarlo.

Esencialmente he creado una clase EntryGrouper que se encarga de la lógica de la agrupación de los valores seleccionados en las listas desplegables y he asumido que las variables section, page & module mantienen esos valores. También he supuesto que ObjectContext.OmniturePageModules es un enumerable de tipo Entry.

Por lo que su consulta LINQ se convierte ahora en estos dos:

var entries = (from entry in ObjectContext.OmniturePageModules 
       where entry.StartOfWeek >= startDate 
        && entry.StartOfWeek <= endDate 
        && (section == "Total" || section == "All" || entry.Section == section) 
        && (page == "Total" || page == "All" || entry.Page == page) 
        && (module == "Total" || module == "All" || entry.Module == module) 
       select entry).ToArray(); // Force query execution 

var grouping = from entry in entries 
       let grouper = new EntryGrouper(entry, section, page, module) 
       group entry by grouper into entryGroup 
       select new 
       { 
        SeriesName = entryGroup.Key.SeriesName, 
        Week = entryGroup.Key.StartOfWeek, 
        Clicks = entryGroup.Sum(p => p.Clicks), 
       }; 

La primera consulta se utiliza para forzar una consulta de selección simple en la base de datos y devolver sólo los registros que desea agrupar. En general, las consultas group by llaman a la base de datos varias veces, por lo que las consultas de este tipo suelen ser mucho más rápidas.

La segunda consulta agrupa los resultados de la primera consulta creando instancias de la clase EntryGrouper como clave de agrupación.

He incluido una propiedad SeriesName en la clase EntryGrouper de manera que toda la lógica de la agrupación se define de forma ordenada en un solo lugar.

Ahora, la clase EntryGrouper es bastante grande como para permitir agrupar a trabajar, que debe tener propiedades para StartOfWeek, Section, Page & Module, y contener sobrecargas de las Equals & GetHashCode métodos, e implementar la interfaz IEquatable<Entry>.

Aquí está:

public class EntryGrouper : IEquatable<Entry> 
{ 
    private Entry _entry; 
    private string _section; 
    private string _page; 
    private string _module; 

    public EntryGrouper(Entry entry, string section, string page, string module) 
    { 
     _entry = entry; 
     _section = section; 
     _page = page; 
     _module = module; 
    } 

    public string SeriesName 
    { 
     get 
     { 
      return String.Format("{0}:{1}:{2}", this.Section, this.Page, this.Module); 
     } 
    } 

    public DateTime StartOfWeek 
    { 
     get 
     { 
      return _entry.StartOfWeek; 
     } 
    } 

    public string Section 
    { 
     get 
     { 
      if (_section == "Total" || _section == "All") 
       return _section; 
      return _entry.Section; 
     } 
    } 

    public string Page 
    { 
     get 
     { 
      if (_page == "Total" || _page == "All") 
       return _page; 
      return _entry.Page; 
     } 
    } 

    public string Module 
    { 
     get 
     { 
      if (_module == "Total" || _module == "All") 
       return _module; 
      return _entry.Module; 
     } 
    } 

    public override bool Equals(object other) 
    { 
     if (other is Entry) 
      return this.Equals((Entry)other); 
     return false; 
    } 

    public bool Equals(Entry other) 
    { 
     if (other == null) 
      return false; 
     if (!EqualityComparer<DateTime>.Default.Equals(this.StartOfWeek, other.StartOfWeek)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Section, other.Section)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Page, other.Page)) 
      return false; 
     if (!EqualityComparer<string>.Default.Equals(this.Module, other.Module)) 
      return false; 
     return true; 
    } 

    public override int GetHashCode() 
    { 
     var hash = 0; 
     hash ^= EqualityComparer<DateTime>.Default.GetHashCode(this.StartOfWeek); 
     hash ^= EqualityComparer<string>.Default.GetHashCode(this.Section); 
     hash ^= EqualityComparer<string>.Default.GetHashCode(this.Page); 
     hash ^= EqualityComparer<string>.Default.GetHashCode(this.Module); 
     return hash; 
    } 

    public override string ToString() 
    { 
     var template = "{{ StartOfWeek = {0}, Section = {1}, Page = {2}, Module = {3} }}"; 
     return String.Format(template, this.StartOfWeek, this.Section, this.Page, this.Module); 
    } 
} 

La lógica de agrupación de esta clase se ve simplemente como esto:

if (_page == "Total" || _page == "All") 
    return _page; 
return _entry.Page; 

Si he entendido mal la forma en que los valores desplegables girar a la agrupación de encendido y apagado, entonces debería solo es necesario cambiar estos métodos, pero el quid de este código es que cuando la agrupación está activada debe devolver un valor de grupo basado en el valor de la entrada y, de lo contrario, debe devolver un valor común para todas las entradas. Si el valor es común para todas las entradas, lógicamente solo crea un único grupo, que es lo mismo que no agrupar en absoluto.

Si tiene más listas desplegables que está agrupando para entonces, necesita agregar más propiedades a la clase EntryGrouper. No olvide agregar estas nuevas propiedades a los métodos Equals & GetHashCode también.

Esta lógica, por lo tanto, representa la agrupación dinámica que usted quería. Por favor, avíseme si he ayudado o si necesita más detalles.

¡Disfrútalo!

+0

Muchas gracias por su amplia respuesta. Trataré de hacerlo mañana y te dejaré saber si funciona para mí; un vistazo rápido es alentador. –

+0

Esto no parece agruparse por StartOfWeek por alguna razón.Tuve que cambiar el código de agrupación para cada columna a if (_section == "All") return _entry.Section; return _section; –

+0

@ 'Daniel Coffman' - No sé por qué no se ha agrupado por' StartOfWeek', debería haberlo hecho. Volví a verificar el código y los métodos 'Equals' y' GetHashCode' utilizan el valor 'StartOfWeek'. Dame un grito si quieres que lo investigue más. Esperaba que el código de agrupación para cada columna probablemente debería ser "ajustado" un poco para sus necesidades. – Enigmativity

8

Aquí está en LINQ dinámica - por supuesto a construir la GroupBy y seleccionar cadenas en tiempo de ejecución:

var double_grouping = (ObjectContext.OmniturePageModules.Where(entry => entry.StartOfWeek >= startDate 
        && entry.StartOfWeek <= endDate 
        && (section == "Total" || section == "All" || entry.Section == section) 
        && (page == "Total" || page == "All" || entry.Page == page) 
        && (module == "Total" || module == "All" || entry.Module == module)) 
        .GroupBy("new (it.Section, it.Page, it.StartOfWeek)", "it")) 
        .Select("new (Sum(Clicks) as Clicks, Key.Section as SeriesSection, Key.Page as SeriesPage, Key.StartOfWeek as Week)"); 

Y aquí es la forma normal de LINQ que se me escapó hasta que un compañero de trabajo lo señaló - este es básicamente la solución de Enigmativity sin la clase mero:

var grouping = (from entry in ObjectContext.OmniturePageModules 
    where entry.StartOfWeek >= startDate && entry.StartOfWeek <= endDate && 
     (section == "Total" || section == "All" || entry.Section == section) && 
     (page == "Total" || page == "All" || entry.Page == page) && 
     (module == "Total" || module == "All" || entry.Module == module) 
    group entry by new 
    { 
     Section = section == "All" ? entry.Section : section, 
     Page = page == "All" ? entry.Page : page, 
     Module = module == "All" ? entry.Module : module, 
     entry.StartOfWeek 
    } 
     into entryGroup 
     select new 
     { 
      SeriesName = 
      entryGroup.Key.Section + ":" + entryGroup.Key.Page + ":" + entryGroup.Key.Module, 
      Week = entryGroup.Key.StartOfWeek, 
      Clicks = entryGroup.Sum(p => p.Clicks) 
     }); 
+0

+1 para dar seguimiento a su solución, ¡gracias! –

0

sé que ha sido un tiempo ya que esta cuestión se publicó, pero tenía que hacer frente a un problema similar recientemente (agrupación dinámica de múltiples columnas seleccionadas por el usuario en tiempo de ejecución) por lo aquí está mi opinión sobre esto.

  1. función auxiliar para crear lambdas agrupación

    static Expression<Func<T, Object>> GetGroupBy<T>(string property) 
    { 
        var data = Expression.Parameter(typeof(T), "data"); 
        var dataProperty = Expression.PropertyOrField(data, property); 
        var conversion = Expression.Convert(dataProperty, typeof(object)); 
        return Expression.Lambda<Func<T, Object>>(conversion, data); 
    } 
    
  2. función para hacer la agrupación en memoria. Devuelve grupos.

    static IEnumerable<IEnumerable<T>> Group<T>(IEnumerable<T> ds, params Func<T, object>[] groupSelectors) 
    { 
        Func<IEnumerable<T>, Func<T, object>[], IEnumerable<IEnumerable<T>>> inner = null; 
        inner = (d, ss) => { 
        if (null == ss || ss.Length == 0) { 
         return new[] { d }; 
        } else { 
         var s = ss.First(); 
         return d.GroupBy(s).Select(g => inner(g.Select(x => x), ss.Skip(1).ToArray())) .SelectMany(x => x); 
        } 
        }; 
        return inner(ds, groupSelectors); 
    } 
    
  3. ¿Cómo se puede utilizar:

    String[] columnsSelectedByUser = ... // contains names of grouping columns selected by user 
    var entries = ... // Force query execution i.e. fetch all data 
    var groupBys = columnsSelectedByUser.Select(x => GetGroupBy(x).Compile()).ToArray(); 
    var grouping = Group(entries, groupBys); // enumerable containing groups of entries 
    

concerniente a actuaciones degradantes, no creo que en realidad es un (gran) problema. Incluso si construyera un SQL de agrupación de forma dinámica, la consulta debería devolver el mismo número de filas que una consulta sin la agrupación. Entonces, aunque en este enfoque, la agrupación no es realizada por la base de datos, el número de filas devuelto por la ejecución forzada de consultas es el mismo que el de la consulta SQL hipotética con criterios de agrupación. Claro, la base de datos probablemente superará a la agrupación en memoria realizada por el código C#, pero la cantidad de tráfico depende únicamente de cuántas filas (entries) deben agruparse.