实体框架“SELECT IN”不使用参数

问题描述 投票:0回答:3

为什么实体框架在使用“SELECT IN”时将文字值放入生成的 SQL 中而不是使用参数:

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 中也会发生这种情况。

c# .net sql-server entity-framework ef-core-2.0
3个回答
17
投票

我不能说为什么 EF(核心)设计者决定在翻译时使用常量而不是变量

Enumerable.Contains
。正如 @Gert Arnold 在评论中指出的,可能与 SQL 查询参数计数限制有关。

有趣的是,当您使用等效的

IN
表达式时,EF (6.2) 和 EF Core (2.1.2) 都会生成带有参数的
||
,例如:

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();

产生所需的翻译。


2
投票

可以使用

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 查询那么容易维护,但是有时为了效率我们必须做出这些妥协。


0
投票

我建议看一下 LINQKit https://github.com/scottksmith95/LINQKit

那么你的代码将如下所示:

using (var context = new TestContext())
{
    var values = new int[] { 1, 2, 3 };
    var predicate = PredicateBuilder.New<Thing>();
    foreach (var value in values)
    {
       predicate = predicate.Or(thing => thing.Id == value);
    }

    var query = context.Things.Where(predicate);

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

您的 SQL 将如下所示:

SELECT
    [Extent1].[Id] AS [Id]
FROM [dbo].[PaymentConfigurations] AS [Extent1]
WHERE [Extent1].[Id] = @__id_0 OR [Extent1].[Id] = @__id_1 OR [Extent1].[Id] = @__id_2
© www.soinside.com 2019 - 2024. All rights reserved.