2010-10-14 2 views
10

Мне нужно перевести следующий запрос LINQ в Dynamic LINQ, который принимает несколько столбцов группировки на основе ввода пользователем. В основном у меня есть группа dropdownlists, которые применяют группировки, и я не хочу перечислять каждую комбинацию групп. Если Dynamic LINQ терпит неудачу, мне, возможно, придется создать SQL-запрос вручную, и никто этого не хочет.Динамический LINQ GroupBy Несколько столбцов

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

Я понятия не имею, как это сделать, как Dynamic LINQ полностью документирована вне «привет мир!» выберите/где/порядок. Я просто не могу понять синтаксис.

Что-то вроде :(?)

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

Я использую класс DynamicQueryable в System.Linq.Dynamic. См: http://weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

Последующая деятельность: решение Enigmativity работал основном. По какой-то причине не хочет группы по DateTime «StartOfWeek» колонка - обходной путь, это просто сделать вторичную группировку:

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

, но это, кажется, серьезно ухудшить производительность ... =/

ответ

3

Если вы явно хотите использовать библиотеку динамических запросов LINQ, то мой ответ не будет тем, что вы хотите, но если вы хотите, чтобы ваше поведение было желательным, и вы счастливы использовать обычный LINQ, то я думаю, что могу помочь.

По существу я создал EntryGrouper класс, который обрабатывает логику группировки по выбранным значениям в выпадающих списках, и я предположил, что переменные section, page & module держать эти значения. Я также предполагал, что ObjectContext.OmniturePageModules является перечислением типа Entry.

Таким образом, ваш запрос LINQ теперь становится эти два:

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

Первый запрос используется, чтобы заставить простого запроса на выборку по базе данных и возвращать только те записи, которые вы хотите сгруппировать. Обычно запросы group by вызывают базу данных несколько раз, поэтому запрос таким образом обычно выполняется намного быстрее.

Второй запрос группирует результаты первого запроса, создавая экземпляры класса EntryGrouper в качестве ключа группировки.

Я включил свойство SeriesName в класс EntryGrouper, чтобы вся логика группировки была четко определена в одном месте.

Теперь EntryGrouper класс довольно велик, как, чтобы позволить группировку работать, он должен иметь свойства StartOfWeek, Section, Page & Module и содержит перегруженные в Equals & GetHashCode методов и реализовать интерфейс IEquatable<Entry>.

Здесь:

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

группирования логика этого класса выглядит просто так:

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

Если я неправильно понял как значения выпадающие повернуть группируя и выключаться, то вы должны просто нужно изменить эти методы, но суть этого кода в том, что при группировке он должен возвращать значение группы на основе значения в записи, и в противном случае он должен возвращать общее значение для всех записей. Если значение является общим для всех записей, то оно логически создает только одну группу, которая не совпадает с группировкой.

Если у вас есть дополнительные выпадающие списки, которые вы группируете, вам нужно добавить дополнительные свойства в класс EntryGrouper. Не забудьте добавить эти новые свойства в методы Equals & GetHashCode.

Эта логика, таким образом, представляет собой динамическую группировку, которую вы хотели. Пожалуйста, дайте мне знать, помогли ли мне или вам нужно больше деталей.

Наслаждайтесь!

+0

Большое спасибо за ваш обширный ответ. Я попробую это завтра и сообщит, будет ли это работать для меня - быстрый взгляд на него обнадеживает. –

+0

По какой-то причине это не похоже на группу StartOfWeek.Мне пришлось изменить код группировки для каждого столбца, если (_section == "All") return _entry.Section; return _section; –

+0

@ 'Даниэль Коффман' - Я не знаю, почему он не сгруппирован по« StartOfWeek », он должен иметь. Я перепроверил код, а методы 'Equals' &' GetHashCode' используют значение «StartOfWeek». Дайте мне крик, если вы хотите, чтобы я заглянул в нее дальше. Я ожидал, что код группировки для каждого столбца, вероятно, потребуется немного «настроить» для ваших нужд. – Enigmativity

8

Здесь в динамической LINQ - вы, конечно, строить GroupBy и выбора строки во время выполнения:

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

А вот нормальный LINQ так, что вырвалось у меня до тех пор, пока Коллега указал на него - это в основном решение Enigmativity без класса морского окуня:

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 для отслеживания вашего решения, спасибо! –

0

Я знаю, что это было время, так как этот вопрос был опубликован, но мне пришлось иметь дело с подобной проблемой в последнее время (динамическую группировки по нескольким столбцам, выбранных пользователем во время выполнения), так вот мое занятие.

  1. Вспомогательная функция для создания группировки Лямбд

    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. Функции для выполнения группировки в оперативной памяти. Возвращает группы.

    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. Как это можно использовать:

    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 
    

Что касается унизительные выступления, я не думаю, что это на самом деле (большая) проблема. Даже если вы построили группировку SQL динамически, запрос должен был бы вернуть такое же количество строк, как запрос без группировки. Поэтому, хотя в этом подходе группировка не выполняется базой данных, количество строк, возвращаемых принудительным выполнением запросов, такое же, как и для гипотетического SQL-запроса с критериями группировки. Конечно, база данных, вероятно, превосходит группировку в памяти, выполняемую кодом C#, но объем трафика зависит только от того, сколько строк (entries) необходимо сгруппировать.

Смежные вопросы