Динамический LINQ GroupBy нескольких столбцов - PullRequest
10 голосов
/ 14 октября 2010

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

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 ]

8 голосов
/ 15 октября 2010

Вот это в Dynamic LINQ - конечно, вы строите строки GroupBy и Select во время выполнения:

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 без класса grouper:

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 )
        } );
3 голосов
/ 14 октября 2010

Если вы явно хотите использовать библиотеку динамических запросов 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 голосов
/ 24 октября 2016

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

  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) нужно сгруппировать.

...