繁体   English   中英

动态LINQ GroupBy多列

[英]Dynamic LINQ GroupBy Multiple Columns

我需要将以下LINQ查询转换为动态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 )
    } );

我不知道如何做到这一点,因为动态LINQ在“你好世界”之外完全没有文档记录! 选择/ where / orderby案例。 我只是无法弄清楚语法。

就像是:(?)

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

我在System.Linq.Dynamic中使用DynamicQueryable类。 请参阅: http//weblogs.asp.net/scottgu/archive/2008/01/07/dynamic-linq-part-1-using-the-linq-dynamic-query-library.aspx

后续行动:Enigmativity的解决方案主要起作用。 由于某种原因,它不希望按日期时间“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 ),
                            } );

但这似乎严重降低了性能...... = /

这是动态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的解决方案:

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

如果您明确想要使用LINQ动态查询库,那么我的答案将不是您想要的,但如果您想要您想要的行为并且您很乐意使用常规LINQ,那么我认为我可以提供帮助。

本质上我已经创建了一个EntryGrouper类,它通过下拉列表中的选定值处理分组逻辑,我假设变量sectionpagemodule保存这些值。 我还假设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查询group by多次调用数据库,因此以这种方式查询通常要快得多。

第二个查询通过创建EntryGrouper类的实例作为分组键来对第一个查询的结果进行分组。

我在EntryGrouper类中包含了一个SeriesName属性,以便在一个地方巧妙地定义所有分组逻辑。

现在, EntryGrouper类非常大,为了允许分组工作,它需要具有StartOfWeekSectionPageModule属性,并包含EqualsGetHashCode方法的重载,并实现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类添加更多属性。 不要忘记将这些新属性添加到EqualsGetHashCode方法中。

因此,此逻辑表示您想要的动态分组。 如果我有帮助或需要更多细节,请告诉我。

请享用!

我知道这个问题已经发布已经有一段时间了,但最近我不得不处理类似的问题(用户在运行时选择的多列动态分组)所以这是我的看法。

  1. 用于创建分组lambda的辅助函数

     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 ie 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 )进行分组。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM