Entity Framework "SELECT IN" 没有使用参数
Entity Framework "SELECT IN" not using parameters
为什么 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 服务器中看到很多缓存的查询计划。有没有办法让 EF 放置参数而不是硬编码值,或者激活参数嗅探是唯一的选择?
它也发生在 EF Core 中。
可以使用 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 查询那样容易维护,但有时为了效率我们不得不做出这些妥协。
我不能说为什么 EF(核心)设计者决定在翻译时使用常量而不是变量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
class 方法。并且为了更容易使用,可以封装在自定义扩展方法中,内部用户 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();
生成所需的翻译。
为什么 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 服务器中看到很多缓存的查询计划。有没有办法让 EF 放置参数而不是硬编码值,或者激活参数嗅探是唯一的选择?
它也发生在 EF Core 中。
可以使用 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 查询那样容易维护,但有时为了效率我们不得不做出这些妥协。
我不能说为什么 EF(核心)设计者决定在翻译时使用常量而不是变量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
class 方法。并且为了更容易使用,可以封装在自定义扩展方法中,内部用户 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();
生成所需的翻译。