Выражение LINQ для возврата значения свойства?

Я пытаюсь создать общую функцию, которая поможет мне выбрать тысячи записей с помощью 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)

person kwcto    schedule 20.02.2009    source источник


Ответы (5)


Я придумал способ разбить запрос на части - т.е. вы даете ему 4000 значений, поэтому он может выполнить 4 запроса по 1000 каждый; с полным примером Northwind. Обратите внимание, что это может не работать в Entity Framework из-за 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();
            }
        }
    }
}
person Marc Gravell    schedule 20.02.2009
comment
Это обрабатывает случай queryable.Where(o => values.Contains(o.propertyToTest)), заменяя его на queryable.InRange(o => o.propertyToTest, blockSize, values) (если я правильно понимаю), но я смотрю на аналогичное переполнение на пределе 2100 параметров, например. queryable.Where(o => !values.Contains(o.propertyToTest)). Я пытаюсь изменить InRange(), чтобы получить эквивалент NotInRange(), и я не уверен, как выполнить логическое отрицание. Я думал о строке foreach (T record in source.Where(lambda))? - person Matt Sach; 30.03.2011
comment
На самом деле, после долгих поисков, я думаю, что нашел то, что нужно, соответственно из ответа, который вы дали месяц назад, на этот: stackoverflow.com/questions/457316/, ref Это также хорошо работает для отмены одной операции: - person Matt Sach; 30.03.2011
comment
@Marc - Как это будет обрабатываться в VB? yield return для нас явно не существует. - person Code Maverick; 22.10.2012
comment
В чем причина того, что размер партии можно настраивать? Разве я не хотел бы, чтобы это всегда было похоже на 2090 год? Так я чуть под пределом 2100 с небольшим количеством игры? - person Dilbert789; 23.07.2013
comment
Инвок не нужен. Вы можете повторно использовать селектор лямбда и параметр, хотя я признаю, что это немного грязно. Я думаю, что отказ от вызова также сделает его пригодным для использования в других ORM. (Я заставил его работать на LLBLGen Pro, используя: gist.github.com/FransBouma/5e7031fe557df4b5b688 - person Frans Bouma; 12.06.2014

Самый простой способ сделать это: использовать 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

Еще раз спасибо за вашу помощь!

person kwcto    schedule 20.02.2009
comment
Мне было бы интересно узнать, что для этого делает TSQL, по сравнению с моим ответом InRange... - person Marc Gravell; 21.02.2009
comment
ВЫБЕРИТЕ [t0].[Id], ... [t0].[Версия] FROM [Refill] AS [t0], ГДЕ [t0].[Id] IN (@p0, @p1, @p2, @p3, @ p4, @p5, @p6, @p7, @p8, @p9) ... Этот запрос 2 раза (20/10 = 2) - person kwcto; 21.02.2009
comment
Что вы предлагаете для blockSize для оптимизации запросов с помощью LinqToSql? Или, другими словами, лучше иметь меньше запросов с большими блоками или больше запросов с меньшими блоками? - person ni5ni6; 24.12.2012
comment
Как правило, вам нужно как можно меньше блоков. Основная проблема заключается в том, что SQL Server допускает только 2100 параметров на запрос/команду. Обычно я использую blockSize 2000. - person kwcto; 04.01.2013

LINQ-to-SQL по-прежнему работает со стандартными параметрами SQL, поэтому написание причудливого выражения не поможет. Здесь есть 3 распространенных варианта:

  • упаковать идентификаторы в (например) csv/tsv; передать как varchar(max) и использовать udf, чтобы разделить его (на сервере) на табличную переменную; присоединиться к переменной таблицы
  • использовать табличный параметр в SQL Server 2008
  • иметь таблицу на сервере, в которую вы могли бы вставлять идентификаторы (возможно, через SqlBulkCopy) (возможно, с «руководством сеанса» или подобным); присоединиться к этому столу

Первый самый простой; получить «split csv udf» тривиально (просто найдите его). Перетащите udf в контекст данных и используйте оттуда.

person Marc Gravell    schedule 20.02.2009
comment
Это не обязательно. Смотрите мой ответ ниже. - person kwcto; 20.02.2009

Передайте 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.

person Muhammad Adnan    schedule 13.04.2012
comment
Вы предполагаете, что коллекция для сопоставления исходит из самой базы данных. Это не всегда так. - person kwcto; 07.05.2012

Вы можете создать свой собственный 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();

        }

    }
person Zero    schedule 07.02.2014