【问题标题】:Dynamic LINQ GroupBy Multiple Columns动态 LINQ GroupBy 多列
【发布时间】:2011-04-25 03:35:17
【问题描述】:

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

我不知道如何做到这一点,因为 Dynamic LINQ 在“hello world!”之外完全没有文档记录。 select/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 ),
                            } );

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

【问题讨论】:

    标签: c# .net linq entity-framework dynamic-linq


    【解决方案1】:

    这里是 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 )
            } );
    

    【讨论】:

      【解决方案2】:

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

      基本上我已经创建了一个EntryGrouper 类来处理按下拉列表中的选定值分组的逻辑,并且我假设变量sectionpagemodule 持有这些值。我还假设ObjectContext.OmniturePageModulesEntry 类型的枚举。

      所以你的 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 类非常大,因为要允许分组工作,它需要具有 StartOfWeekSectionPageModule 的属性,并包含 @987654337 的重载@&GetHashCode 方法,并实现IEquatable&lt;Entry&gt; 接口。

      这里是:

      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 方法中。

      因此,此逻辑表示您想要的动态分组。如果我提供了帮助或者您需要更多详细信息,请告诉我。

      享受吧!

      【讨论】:

      • 非常感谢您的广泛回答。我明天试试这个,让你知道它是否适合我——快速浏览一下是令人鼓舞的。
      • 由于某种原因,这似乎没有按 StartOfWeek 分组。我必须将每列的分组代码更改为 if(_section == "All") return _entry.Section;返回_section;
      • @Daniel Coffman - 我不知道为什么它没有按StartOfWeek 分组,它应该有。我重新检查了代码,EqualsGetHashCode 方法使用StartOfWeek 值。如果您希望我进一步调查,请给我大喊。我曾预计每列的分组代码可能需要根据您的需要进行一些“调整”。
      【解决方案3】:

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

      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 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)。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-02-28
        相关资源
        最近更新 更多