Entity Framework SELECT IN не использует параметры

Почему Entity Framework помещает литеральные значения в сгенерированный SQL, а не использует параметры при использовании «SELECT IN»:

using (var context = new TestContext())
{
    var values = new int[] { 1, 2, 3 };
    var query = context.Things.Where(x => values.Contains(x.Id));

    Console.WriteLine(query.ToString());
}

Это производит следующий SQL:

SELECT
    [Extent1].[Id] AS [Id]
    FROM [dbo].[PaymentConfigurations] AS [Extent1]
    WHERE [Extent1].[Id] IN (1, 2, 3)

Я вижу много кэшированных планов запросов в SQL Server. Можно ли каким-либо образом заставить EF помещать параметры, а не закодированные значения, или активация обнюхивания параметров является единственным вариантом?

Это происходит и в EF Core.


person Vlad    schedule 28.08.2018    source источник
comment
На уровне движка предложения IN не могут быть параметризованы.   -  person Alex K.    schedule 28.08.2018
comment
Почему именно вы хотите параметризовать сгенерированный SQL? Конечно, обычные причины не применимы, когда вы используете ORM по назначению?   -  person andrensairr    schedule 28.08.2018
comment
@andrensairr, чтобы избежать кэширования плана запроса для каждой комбинации значений в in. Всегда меньше 10 параметров, поэтому я ожидаю, что будет кэшировано около 10 планов запросов, по одному на количество параметров ... скорее фактические 1500+, которые Я вижу в мониторе активности.   -  person Vlad    schedule 28.08.2018
comment
At the engine level IN clauses cannot be parameterized. Какой движок имеет в виду @AlexK.? SQL Server определенно позволяет использовать параметры в предложениях IN.   -  person mjwills    schedule 28.08.2018
comment
Рассмотрите возможность использования параметра конфигурации сервера оптимизации для специальных рабочих нагрузок docs.microsoft.com/en-us/sql/database-engine/configure-windows/   -  person Denis Rubashkin    schedule 28.08.2018
comment
LINQ-to-SQL сделал это. Я думаю, что от него отказались из-за максимального количества параметров, разрешенных в одном запросе (2100).   -  person Gert Arnold    schedule 28.08.2018


Ответы (2)


Я не могу сказать, почему разработчики EF (Core) решили использовать константы вместо переменных при переводе Enumerable.Contains. Как указал @Gert Arnold в комментариях, это может быть связано с ограничением количества параметров SQL-запроса.

Интересно, что и EF (6.2), и EF Core (2.1.2) генерируют IN с параметрами при использовании эквивалентного выражения ||, например:

var values = new int[] { 1, 2, 3 };
var value0 = values[0];
var value1 = values[1];
var value2 = values[2]; 
var query = context.Things.Where(x =>
    x.Id == value0 ||
    x.Id == value1 ||
    x.Id == value2);

Сгенерированный запрос EF6.2

SELECT
    [Extent1].[Id] AS [Id]
    FROM [dbo].[Things] AS [Extent1]
    WHERE [Extent1].[Id] IN (@p__linq__0,@p__linq__1,@p__linq__2)

EF Core 2.1 делает нечто подобное.

Таким образом, решение состоит в том, чтобы преобразовать выражение Contains в выражение на основе ||. Это должно быть динамически с использованием методов класса Expression. И чтобы сделать его более простым в использовании, он может быть инкапсулирован в пользовательский метод расширения, который внутренне пользователь ExpressionVisitor выполняет преобразование.

Что-то вроде этого:

public static partial class EfQueryableExtensions
{
    public static IQueryable<T> Parameterize<T>(this IQueryable<T> source)
    {
        var expression = new ContainsConverter().Visit(source.Expression);
        if (expression == source) return source;
        return source.Provider.CreateQuery<T>(expression);
    }

    class ContainsConverter : ExpressionVisitor
    {
        protected override Expression VisitMethodCall(MethodCallExpression node)
        {
            if (node.Method.DeclaringType == typeof(Enumerable) &&
                node.Method.Name == nameof(Enumerable.Contains) &&
                node.Arguments.Count == 2 &&
                CanEvaluate(node.Arguments[0]))
            {
                var values = Expression.Lambda<Func<IEnumerable>>(node.Arguments[0]).Compile().Invoke();
                var left = Visit(node.Arguments[1]);
                Expression result = null;
                foreach (var value in values)
                {
                    // var variable = new Tuple<TValue>(value);
                    var variable = Activator.CreateInstance(typeof(Tuple<>).MakeGenericType(left.Type), value);
                    // var right = variable.Item1;
                    var right = Expression.Property(Expression.Constant(variable), nameof(Tuple<int>.Item1));
                    var match = Expression.Equal(left, right);
                    result = result != null ? Expression.OrElse(result, match) : match;
                }
                return result ?? Expression.Constant(false);
            }
            return base.VisitMethodCall(node);
        }

        static bool CanEvaluate(Expression e)
        {
            if (e == null) return true;
            if (e.NodeType == ExpressionType.Convert)
                return CanEvaluate(((UnaryExpression)e).Operand);
            if (e.NodeType == ExpressionType.MemberAccess)
                return CanEvaluate(((MemberExpression)e).Expression);
            return e.NodeType == ExpressionType.Constant;
        }
    }
}

Применение его к примеру запроса

var values = new int[] { 1, 2, 3 };
var query = context.Things
    .Where(x => values.Contains(x.Id))
    .Parameterize();

производит нужный перевод.

person Ivan Stoev    schedule 28.08.2018
comment
Гениально (опять же)! Обратите внимание, что другое условие || для другого свойства может превратить создание IN в обычную цепочку предикатов OR. Но в любом случае, он все еще параметризуется. - person Gert Arnold; 31.08.2018
comment
извините, я получаю эту ошибку: LINQ to Entities не распознает метод System.Linq.IQueryable1[EntityModel.tblParamNameProduct] Parameterize[tblParamNameProduct](System.Linq.IQueryable1[EntityModel.tblParamNameProduct]), и этот метод не может быть преобразован в выражение хранилища. Вероятно, потому что мне нужно продолжить с другим .Select() после .Parameterize(). Когда я вставляю .ToArray() между .Parameterize() и другим .Select(), все работает отлично, спасибо. - person or hor; 02.03.2021
comment
Что приятно: запрос пареметризован предложением IN(@p__linq__0,@p__linq__1,...). - person or hor; 02.03.2021
comment
@orhor Проблема не в дополнительных операторах LINQ после вызова Parameterize(), а в том, что сам вызов является частью другого дерева выражения запроса, и в этом случае он вообще не вызывается, а просто запоминается, и тогда, конечно, переводчик сообщает о нем как о неизвестном метод. Его действительно нужно применять к корню запроса верхнего уровня, иначе он не будет работать. Проще всего вызвать его в финальном запросе — он не требует DbSet, работает с любым запросом, в том числе с пользовательской проекцией (Select) и т. д. - person Ivan Stoev; 02.03.2021

Параметризованные запросы с IN можно выполнять, хотя это и несколько обходной путь. Вам нужно будет использовать прямой SQL-запрос и сгенерировать параметризованный SQL вручную, примерно так:

var values = new object[] { 1, 2, 3 };
var idx = 0;
var query = context.Things.SqlQuery($@"
    SELECT
        [Extent1].[Id] AS [Id]
    FROM [dbo].[PaymentConfigurations] AS [Extent1]
    WHERE [Extent1].[Id] IN ({string.Join(",", values.Select(i => $"@p{idx++}"))})",
    values);

Сгенерированный список имен параметров напрямую встраивается в SQL, используемый в запросе, и получает значения с помощью параметра values. Обратите внимание, что вам может потребоваться убедиться, что ваш массив values является object[], а не int[], чтобы убедиться, что он распакован в параметры SqlQuery. Этот подход не так прост в обслуживании, как запрос LINQ, однако иногда приходится идти на эти компромиссы ради эффективности.

person andrensairr    schedule 28.08.2018