代码之家  ›  专栏  ›  技术社区  ›  Daniel Coffman

动态Linq Groupby多列

  •  10
  • Daniel Coffman  · 技术社区  · 14 年前

    我需要将下面的LINQ查询转换为动态LINQ,它接受基于用户输入的多个分组列。基本上,我有一组应用分组的DropDownlist,我不想枚举分组的每个组合。如果动态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在“Hello World”之外完全没有文档记录。选择/where/orderby cases。我就是搞不懂语法。

    比如:(?)

    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

    跟进: 神秘性的解决方案有效 主要地 . 出于某种原因,它不希望按日期时间“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 年前
        1
  •  3
  •   Enigmativity    14 年前

    如果您明确地想要使用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),
                   };
    

    第一个查询用于强制对数据库执行简单的select查询,并仅返回要分组的记录。一般来说 group by 查询多次调用数据库,因此以这种方式进行查询通常要快得多。

    第二个查询通过创建 入口石斑鱼 类作为分组键。

    我已经包括了 SeriesName 中的属性 入口石斑鱼 类,以便在一个位置整齐地定义所有分组逻辑。

    现在, 入口石斑鱼 类非常大,因为为了允许分组工作,它需要具有 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;
    

    如果我误解了下拉值是如何打开和关闭分组的,那么您应该只需要更改这些方法,但这段代码的关键在于,当分组打开时,它应该根据条目中的值返回一个组值,否则它应该为所有条目返回一个公共值。如果该值对于所有条目都是通用的,那么逻辑上它只创建一个与完全不分组相同的组。

    如果要分组的下拉列表更多,则需要向 入口石斑鱼 班级。不要忘记将这些新属性添加到 等于 和; 方法 方法也是如此。

    因此,这个逻辑表示您想要的动态分组。请告诉我我是否帮助过你,或者你是否需要更多的细节。

    享受!

        2
  •  8
  •   Daniel Coffman    14 年前

    这里是动态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方法,在我的同事指出之前,它一直在逃避我——这基本上是没有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
  •  0
  •   danielK    8 年前

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

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

      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 )必须分组。

    推荐文章