【问题标题】:LINQ Expression to return Property value?LINQ 表达式返回属性值?
【发布时间】:2009-02-20 01:27:22
【问题描述】:

我正在尝试创建一个通用函数来帮助我使用 LINQ to SQL 从本地列表中选择数千条记录。 SQL Server(至少 2005 年)将查询限制为 2100 个参数,我想选择更多的记录。

这是一个很好的用法示例:

var some_product_numbers = new int[] { 1,2,3 ... 9999 };

Products.SelectByParameterList(some_product_numbers, p => p.ProductNumber);

这是我的(非工作)实现:

public static IEnumerable<T> SelectByParameterList<T, PropertyType>(Table<T> items, 

IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> property) where T : class
{
    var groups = parameterList
        .Select((Parameter, index) =>
            new
            {
                GroupID = index / 2000, //2000 parameters per request
                Parameter
            }
        )
        .GroupBy(x => x.GroupID)
        .AsEnumerable();

    var results = groups
    .Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
    .SelectMany(g => 
        /* THIS PART FAILS MISERABLY */
        items.Where(item => g.Parameters.Contains(property.Compile()(item)))
    );

    return results;
}

我见过很多使用表达式构建谓词的例子。在这种情况下,我只想执行委托以返回当前 ProductNumber 的值。或者更确切地说,我想把它翻译成 SQL 查询(它在非泛型形式下工作得很好)。

我知道编译表达式只会让我回到第一方(将委托作为 Func 传递),但我不确定如何将参数传递给“未编译”的表达式。

感谢您的帮助!

**** 编辑:** 让我进一步澄清:

这是我想要概括的一个工作示例:

var local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray();

var groups = local_refill_ids
    .Select((Parameter, index) =>
        new
        {
            GroupID = index / 5, //5 parameters per request
            Parameter
        }
    )
    .GroupBy(x => x.GroupID)
    .AsEnumerable();

var results = groups
.Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
.SelectMany(g => 
    Refills.Where(r => g.Parameters.Contains(r.Id))
)
.ToArray()
;

此 SQL 代码中的结果:

SELECT [t0].[Id], ... [t0].[Version]
FROM [Refill] AS [t0]
WHERE [t0].[Id] IN (@p0, @p1, @p2, @p3, @p4)

... That query 4 more times (20 / 5 = 4)

【问题讨论】:

    标签: c# linq linq-to-sql lambda expression


    【解决方案1】:

    我想出了一种将查询分块的方法——即你给它 4000 个值,所以它可能会执行 4 个请求,每个请求 1000 个;带有完整的罗斯文示例。请注意,由于Expression.Invoke,这可能不适用于实体框架 - 但在 LINQ to SQL 上很好:

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    
    namespace ConsoleApplication5 {
        /// SAMPLE USAGE
        class Program {
            static void Main(string[] args) {
                // get some ids to play with...
                string[] ids;
                using(var ctx = new DataClasses1DataContext()) {
                    ids = ctx.Customers.Select(x => x.CustomerID)
                        .Take(100).ToArray();
                }
    
                // now do our fun select - using a deliberately small
                // batch size to prove it...
                using (var ctx = new DataClasses1DataContext()) {
                    ctx.Log = Console.Out;
                    foreach(var cust in ctx.Customers
                            .InRange(x => x.CustomerID, 5, ids)) {
                        Console.WriteLine(cust.CompanyName);
                    }
                }
            }
        }
    
        /// THIS IS THE INTERESTING BIT
        public static class QueryableChunked {
            public static IEnumerable<T> InRange<T, TValue>(
                    this IQueryable<T> source,
                    Expression<Func<T, TValue>> selector,
                    int blockSize,
                    IEnumerable<TValue> values) {
                MethodInfo method = null;
                foreach(MethodInfo tmp in typeof(Enumerable).GetMethods(
                        BindingFlags.Public | BindingFlags.Static)) {
                    if(tmp.Name == "Contains" && tmp.IsGenericMethodDefinition
                            && tmp.GetParameters().Length == 2) {
                        method = tmp.MakeGenericMethod(typeof (TValue));
                        break;
                    }
                }
                if(method==null) throw new InvalidOperationException(
                    "Unable to locate Contains");
                foreach(TValue[] block in values.GetBlocks(blockSize)) {
                    var row = Expression.Parameter(typeof (T), "row");
                    var member = Expression.Invoke(selector, row);
                    var keys = Expression.Constant(block, typeof (TValue[]));
                    var predicate = Expression.Call(method, keys, member);
                    var lambda = Expression.Lambda<Func<T,bool>>(
                          predicate, row);
                    foreach(T record in source.Where(lambda)) {
                        yield return record;
                    }
                }
            }
            public static IEnumerable<T[]> GetBlocks<T>(
                    this IEnumerable<T> source, int blockSize) {
                List<T> list = new List<T>(blockSize);
                foreach(T item in source) {
                    list.Add(item);
                    if(list.Count == blockSize) {
                        yield return list.ToArray();
                        list.Clear();
                    }
                }
                if(list.Count > 0) {
                    yield return list.ToArray();
                }
            }
        }
    }
    

    【讨论】:

    • 这处理queryable.Where(o =&gt; values.Contains(o.propertyToTest)) 的情况,用queryable.InRange(o =&gt; o.propertyToTest, blockSize, values) 替换它(如果我理解正确的话),但我正在查看2100 参数限制的类似溢出,例如queryable.Where(o =&gt; !values.Contains(o.propertyToTest))。我正在尝试修改 InRange() 以获得 NotInRange() 等效项,但我不确定如何进行布尔否定。我的想法是在foreach (T record in source.Where(lambda)) 行?
    • 实际上,经过多次寻找,我想我已经找到了需要的东西,从你一个月前给这个答案的答案中适当地找到了:stackoverflow.com/questions/457316/…, ref "这也适用于否定单个操作:"
    • @Marc - 这将如何在 VB 中处理? yield return 显然对我们不存在。
    • 批量大小可配置的原因是什么?我不是一直希望这像 2090 年那样吗?所以我只是在 2100 的限制下有一点玩?
    • 调用不是必需的。您可以重复使用选择器 lambda 和参数,尽管我承认它有点脏。避免调用也会使它在我认为的其他 ORM 上可用。 (我在 LLBLGen Pro 上使用:gist.github.com/FransBouma/5e7031fe557df4b5b688
    【解决方案2】:

    最简单的方法:使用LINQKit(免费、非限制性许可)

    代码的工作版本:

    public static IEnumerable<T> SelectByParameterList<T, PropertyType>(this Table<T> items, IEnumerable<PropertyType> parameterList, Expression<Func<T, PropertyType>> propertySelector, int blockSize) where T : class
    {
        var groups = parameterList
            .Select((Parameter, index) =>
                new
                {
                    GroupID = index / blockSize, //# of parameters per request
                    Parameter
                }
            )
            .GroupBy(x => x.GroupID)
            .AsEnumerable();
    
        var selector = LinqKit.Linq.Expr(propertySelector);
    
        var results = groups
        .Select(g => new { Group = g, Parameters = g.Select(x => x.Parameter) } )
        .SelectMany(g => 
            /* AsExpandable() extension method requires LinqKit DLL */
            items.AsExpandable().Where(item => g.Parameters.Contains(selector.Invoke(item)))
        );
    
        return results;
    }
    

    示例用法:

        Guid[] local_refill_ids = Refills.Select(r => r.Id).Take(20).ToArray();
    
        IEnumerable<Refill> results = Refills.SelectByParameterList(local_refill_ids, r => r.Id, 10); //runs 2 SQL queries with 10 parameters each
    

    再次感谢您的帮助!

    【讨论】:

    • 与我的 InRange 回复相比,我会对 TSQL 的作用感兴趣...
    • SELECT [t0].[Id], ... [t0].[Version] FROM [Refill] AS [t0] WHERE [t0].[Id] IN (@p0, @p1 , @p2, @p3, @p4, @p5, @p6, @p7, @p8, @p9) ... 查询 2 次 (20 / 10 = 2)
    • 您对 blockSize 有什么建议,以便使用 LinqToSql 优化查询?或者,换一种说法,大块的查询越少越好,还是小块的查询多?
    • 您通常希望尽可能少的块。主要问题是 SQL Server 每个查询/命令只允许 2100 个参数。我通常会使用 2000 的块大小。
    【解决方案3】:

    LINQ-to-SQL 仍可通过标准 SQL 参数工作,因此编写花哨的表达式不会有帮助。这里有 3 个常用选项:

    • 将 id 打包到(例如)csv/tsv 中;作为varchar(max) 向下传递并使用udf 将其(在服务器上)拆分为表变量;加入表变量
    • 在 SQL Server 2008 中使用表值参数
    • 在服务器上有一个表,您可以将 ID 推送到其中(可能通过 SqlBulkCopy)(可能使用“会话 guid”或类似的);加入此表

    第一种是最简单的;获得“拆分 csv udf”是微不足道的(只需搜索它)。将 udf 拖到数据上下文中并从那里使用。

    【讨论】:

    • 这不是必需的。请参阅下面的答案。
    【解决方案4】:

    IQuerable 传递给Contains 函数,而不是列表或数组。请看下面的例子

    var df_handsets = db.DataFeed_Handsets.Where(m => m.LaunchDate != null).
                      Select(m => m.Name);
    var Make = (from m in db.MobilePhones
        where (m.IsDeleted != true || m.IsDeleted == null)
            && df_handsets.Contains(m.Name)
        orderby m.Make
        select new { Value = m.Make, Text = m.Make }).Distinct();
    

    当你传递列表或数组时,它以参数的形式传递,当列表项计数大于2100时,它会超过计数。

    【讨论】:

    • 您假设要匹配的集合来自数据库本身。情况并非总是如此。
    【解决方案5】:

    您可以创建自己的 QueryProvider

    public class QueryProvider : IQueryProvider
    {
        // Translates LINQ query to SQL.
        private readonly Func<IQueryable, DbCommand> _translator;
    
        // Executes the translated SQL and retrieves results.
        private readonly Func<Type, string, object[], IEnumerable> _executor;
    
        public QueryProvider(
            Func<IQueryable, DbCommand> translator,
            Func<Type, string, object[], IEnumerable> executor)
        {
    
            this._translator = translator;
            this._executor = executor;
        }
    
        #region IQueryProvider Members
    
        public IQueryable<TElement> CreateQuery<TElement>(Expression expression)
        {
            return new Queryable<TElement>(this, expression);
        }
    
        public IQueryable CreateQuery(Expression expression)
        {
            throw new NotImplementedException();
        }
    
        public TResult Execute<TResult>(Expression expression)
        {
            bool isCollection = typeof(TResult).IsGenericType &&
                typeof(TResult).GetGenericTypeDefinition() == typeof(IEnumerable<>);
            var itemType = isCollection
                // TResult is an IEnumerable`1 collection.
                ? typeof(TResult).GetGenericArguments().Single()
                // TResult is not an IEnumerable`1 collection, but a single item.
                : typeof(TResult);
            var queryable = Activator.CreateInstance(
                typeof(Queryable<>).MakeGenericType(itemType), this, expression) as IQueryable;
    
            IEnumerable queryResult;
    
            // Translates LINQ query to SQL.
            using (var command = this._translator(queryable))
            {
                var parameters = command.Parameters.OfType<DbParameter>()
                    .Select(parameter => parameter)
                    .ToList();
    
                var query = command.CommandText;
                var newParameters = GetNewParameterList(ref query, parameters);
    
                queryResult = _executor(itemType,query,newParameters);
            }
    
            return isCollection
                ? (TResult)queryResult // Returns an IEnumerable`1 collection.
                : queryResult.OfType<TResult>()
                             .SingleOrDefault(); // Returns a single item.
        }       
    
        public object Execute(Expression expression)
        {
            throw new NotImplementedException();
        }
    
        #endregion
    
         private static object[] GetNewParameterList(ref string query, List<DbParameter> parameters)
        {
            var newParameters = new List<DbParameter>(parameters);
    
            foreach (var dbParameter in parameters.Where(p => p.DbType == System.Data.DbType.Int32))
            {
                var name = dbParameter.ParameterName;
                var value = dbParameter.Value != null ? dbParameter.Value.ToString() : "NULL";
                var pattern = String.Format("{0}[^0-9]", dbParameter.ParameterName);
                query = Regex.Replace(query, pattern, match => value + match.Value.Replace(name, ""));
                newParameters.Remove(dbParameter);
            }
    
            for (var i = 0; i < newParameters.Count; i++)
            {
                var parameter = newParameters[i];
                var oldName = parameter.ParameterName;
                var pattern = String.Format("{0}[^0-9]", oldName);
                var newName = "@p" + i;
                query = Regex.Replace(query, pattern, match => newName + match.Value.Replace(oldName, ""));
            }      
    
            return newParameters.Select(x => x.Value).ToArray();
        }
    }
    
    
        static void Main(string[] args)
        {
            using (var dc=new DataContext())
            {
                var provider = new QueryProvider(dc.GetCommand, dc.ExecuteQuery);
    
                var serviceIds = Enumerable.Range(1, 2200).ToArray();
    
                var tasks = new Queryable<Task>(provider, dc.Tasks).Where(x => serviceIds.Contains(x.ServiceId) && x.CreatorId==37 && x.Creator.Name=="12312").ToArray();
    
            }
    
        }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-07-26
      • 2021-09-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多