动态LINQ按多列分组

10

我需要将以下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 )
    } );

我完全不知道如何做到这一点,因为除了“hello world!”之类的select/where/orderby情况外,动态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)");

我正在使用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的解决方案在大部分情况下都有效。但由于某种原因,它不想按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个回答

9

以下是使用动态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
如果您明确想要使用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 类的实例作为分组键来对第一个查询的结果进行分组。
我在 EntryGrouper 类中包含了 SeriesName 属性,以便所有分组逻辑都可以整洁地定义在一个地方。
现在,EntryGrouper 类相当大,因为为了使分组工作,它需要具有 StartOfWeek、Section、Page 和 Module 属性,并包含 Equals 和 GetHashCode 方法的重载,并实现 IEquatable 接口。
以下是它的代码:
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方法中。
因此,这个逻辑代表了您想要的动态分组。请告诉我是否有所帮助,或者您需要更多细节。
享受吧!

非常感谢您详尽的回答。我明天会尝试一下,如果对我有效,我会告诉您的——初步浏览看起来很有希望。 - Daniel Coffman
出于某种原因,这似乎无法按StartOfWeek分组。我不得不为每个列更改分组代码,以if(_section ==“All”)返回_entry.Section;否则返回_section; - Daniel Coffman
@Daniel Coffman - 我不知道为什么它没有按 StartOfWeek 进行分组,它应该有的。我重新检查了代码,EqualsGetHashCode 方法使用了 StartOfWeek 值。如果你想让我进一步调查,请告诉我。我预计每个列的分组代码可能需要针对你的需求进行一些“微调”。 - Enigmativity

1

我知道这个问题发布已经有一段时间了,但是最近我也遇到了一个类似的问题(动态运行时由用户选择多列进行分组),所以我来分享我的思路。

创建分组lambda的帮助函数
``` static Expression> GetGroupBy(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>(conversion, data); } ```
执行内存分组的方法,返回分组结果
``` static IEnumerable> Group(IEnumerable ds, params Func[] groupSelectors) { Func, Func[], IEnumerable>> 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); } ```
如何使用:
``` String[] columnsSelectedByUser = ... // 包含用户选择的分组列名称 var entries = ... // 强制查询执行即获取所有数据 var groupBys = columnsSelectedByUser.Select(x => GetGroupBy(x).Compile()).ToArray(); var grouping = Group(entries, groupBys); // 包含条目组的枚举
关于性能退化的问题,我认为这并不是一个(大)问题。即使您动态构建分组 SQL,查询返回的行数也必须与没有分组的查询相同。因此,尽管在这种方法中分组不是由数据库完成的,但由强制执行查询返回的行数与具有分组条件的假设 SQL 查询所返回的行数相同。当然,数据库可能会优于 C# 代码执行的内存中分组,但流量量仅取决于必须进行分组的行数(entries)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接