2010-10-14 22 views
10

J'ai besoin de traduire la requête LINQ suivante en Dynamic LINQ qui accepte plusieurs colonnes de regroupement en fonction de l'entrée de l'utilisateur. Fondamentalement, j'ai un tas de listes déroulantes qui appliquent des groupements et je ne veux pas énumérer toutes les combinaisons de groupements. Si Dynamic LINQ échoue, je peux avoir à construire une requête SQL manuellement, et personne ne veut cela.Dynamic LINQ GroupBy Colonnes multiples

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

Je n'ai pas la moindre idée comment faire cela comme dynamique LINQ est totalement en situation irrégulière en dehors du « Bonjour tout le monde! » select/where/orderby cas. Je ne peux pas comprendre la syntaxe.

Quelque chose comme :(?)

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

J'utilise la classe DynamicQueryable dans System.Linq.Dynamic. Voir: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Suivi: La solution de Enigmativity a travaillé la plupart du temps. Pour une raison quelconque, il ne veut pas le groupe par la colonne datetime « StartOfWeek » - solution de contournement est juste pour faire un groupe secondaire:

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

mais cela semble se dégrader sérieusement les performances ... =/

Répondre

3

Si vous souhaitez explicitement utiliser la bibliothèque de requêtes dynamique LINQ, alors ma réponse ne sera pas ce que vous voulez, mais si vous voulez le comportement souhaité et que vous êtes heureux d'utiliser LINQ standard, je pense que je peux vous aider.

Essentiellement, j'ai créé une classe EntryGrouper qui gère la logique de regroupement par les valeurs sélectionnées dans les listes déroulantes et je l'ai supposé que les variables section, page & module détiennent ces valeurs. J'ai également supposé que ObjectContext.OmniturePageModules est un énumérable de type Entry.

Ainsi, votre requête LINQ devient maintenant ces deux:

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 première requête est utilisée pour forcer une simple requête de sélection sur la base de données et retourner uniquement les enregistrements que vous souhaitez regrouper. Généralement, les requêtes group by appellent la base de données plusieurs fois, ce qui fait que l'interrogation de cette manière est généralement beaucoup plus rapide.

La deuxième requête regroupe les résultats de la première requête en créant des instances de la classe EntryGrouper en tant que clé de regroupement.

J'ai inclus une propriété SeriesName dans la classe EntryGrouper afin que toute la logique de regroupement soit bien définie au même endroit.

Maintenant, la classe EntryGrouper est assez grand que, pour permettre le regroupement au travail, il doit avoir des propriétés pour StartOfWeek, Section, Page & Module et contiennent les surcharges des Equals & GetHashCode méthodes et mettre en œuvre l'interface IEquatable<Entry>.

Ici, il 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 logique de regroupement de cette classe ressemble simplement ceci:

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

Si j'ai mal compris la façon dont vous les valeurs de liste déroulante tourner le regroupement sur et en dehors, alors vous devriez juste besoin de changer ces méthodes, mais le point crucial de ce code est que lorsque le groupement est activé, il doit renvoyer une valeur de groupe basée sur la valeur de l'entrée et sinon il doit retourner une valeur commune pour toutes les entrées. Si la valeur est commune à toutes les entrées, elle ne crée logiquement qu'un seul groupe, ce qui revient à ne pas le regrouper du tout.

Si vous avez plus de listes déroulantes que vous regroupez, vous devez ajouter plus de propriétés à la classe EntryGrouper. N'oubliez pas d'ajouter ces nouvelles propriétés aux méthodes Equals & GetHashCode. Cette logique représente donc le groupement dynamique que vous vouliez. S'il vous plaît laissez-moi savoir si j'ai aidé ou si vous avez besoin de plus de détails.

Profitez-en!

+0

Merci beaucoup pour votre réponse complète. Je vais essayer ça demain et vous faire savoir si cela fonctionne pour moi - un coup d'œil rapide est encourageant. –

+0

Cela ne semble pas grouper par StartOfWeek pour une raison quelconque.J'ai dû changer le code de regroupement pour chaque colonne à if (_section == "All") return _entry.Section; return _section; –

+0

@ 'Daniel Coffman' - Je ne sais pas pourquoi il n'a pas été regroupé par' StartOfWeek', il aurait dû. J'ai revérifié le code et les méthodes 'Equals' &' GetHashCode' utilisent la valeur 'StartOfWeek'. Donnez-moi un cri si vous voulez que je l'examine plus avant. Je m'attendais à ce que le code de regroupement pour chaque colonne ait besoin d'être "modifié" un peu pour vos besoins. – Enigmativity

8

Ici, il est en dynamique LINQ - Bien sûr, vous construisez la GroupBy et des chaînes Sélectionnez à l'exécution:

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

Et voici la façon LINQ normale qui me a échappé jusqu'à ce qu'un collègue de travail l'a fait remarquer - ce est essentiellement la solution de Enigmativity sans la classe mérou:

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 pour le suivi de votre solution, merci! –

0

Je sais que ça fait un moment que cette question a été publiée, mais je devais faire face à un problème similaire récemment (regroupement dynamique par plusieurs colonnes sélectionnées par l'utilisateur dans l'exécution) afin voici mon point de vue.

  1. fonction d'assistance pour la création de regroupement lambdas

    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. Fonction pour faire le regroupement en mémoire. Renvoie des groupes.

    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. Comment serait-il utilisé: problème

    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 
    

En ce qui concerne les performances dégradantes, je ne pense pas que ce soit en fait un (grand). Même si vous avez construit un SQL de manière dynamique, la requête doit renvoyer le même nombre de lignes qu'une requête sans le regroupement. Ainsi, bien que dans cette approche, le regroupement ne soit pas effectué par la base de données, le nombre de lignes renvoyées par l'exécution forcée de requêtes est le même que pour la requête SQL hypothétique avec des critères de regroupement. Bien sûr, la base de données surpasserait probablement le regroupement en mémoire effectué par le code C#, mais la quantité de trafic dépend uniquement du nombre de lignes (entries) à grouper.