Force Entity Framework使用SQL参数化来更好地重用SQL proc缓存

entity framework似乎总是在生成的SQL中使用常量来提供给Skip()Take()

在下面的简化示例中:

 int x = 10; int y = 10; var stuff = context.Users .OrderBy(u => u.Id) .Skip(x) .Take(y) .Select(u => u.Id) .ToList(); x = 20; var stuff2 = context.Users .OrderBy(u => u.Id) .Skip(x) .Take(y) .Select(u => u.Id) .ToList(); 

上面的代码生成以下SQL查询:

 SELECT TOP (10) [Extent1].[Id] AS [Id] FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number] FROM [dbo].[User] AS [Extent1] ) AS [Extent1] WHERE [Extent1].[row_number] > 10 ORDER BY [Extent1].[Id] ASC SELECT TOP (10) [Extent1].[Id] AS [Id] FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number] FROM [dbo].[User] AS [Extent1] ) AS [Extent1] WHERE [Extent1].[row_number] > 20 ORDER BY [Extent1].[Id] ASC 

导致2个Adhoc计划添加到SQL proc缓存中,每个计划使用1个。

我想要完成的是参数化Skip()Take()逻辑,以便生成以下SQL查询:

 EXEC sp_executesql N'SELECT TOP (@p__linq__0) [Extent1].[Id] AS [Id] FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number] FROM [dbo].[User] AS [Extent1] ) AS [Extent1] WHERE [Extent1].[row_number] > @p__linq__1 ORDER BY [Extent1].[Id] ASC',N'@p__linq__0 int,@p__linq__1 int',@p__linq__0=10,@p__linq__1=10 EXEC sp_executesql N'SELECT TOP (@p__linq__0) [Extent1].[Id] AS [Id] FROM ( SELECT [Extent1].[Id] AS [Id], row_number() OVER (ORDER BY [Extent1].[Id] ASC) AS [row_number] FROM [dbo].[User] AS [Extent1] ) AS [Extent1] WHERE [Extent1].[row_number] > @p__linq__1 ORDER BY [Extent1].[Id] ASC',N'@p__linq__0 int,@p__linq__1 int',@p__linq__0=10,@p__linq__1=20 

这导致1个准备计划添加到SQL proc缓存中,有2次使用。

我有一些相当复杂的查询,并且在第一次运行时遇到了很大的开销(在SQL Server端),并且在后续运行时执行得更快(因为它可以使用计划缓存)。 请注意,这些更高级的查询已经使用sp_executesql,因为其他值已参数化,因此我不关心该方面。

上面生成的第一组查询基本上意味着任何分页逻辑都会在每个页面的计划缓存中创建一个新条目,膨胀缓存并要求为每个页面产生计划生成开销。

我可以强制Entity Framework参数化值吗? 我注意到其他值,例如Where子句,有时它参数化值,有时它使用常量。

我完全出去吃午饭吗? 有没有理由为什么entity framework的现有行为比我想要的行为更好?

编辑:如果它是相关的,我应该提到我正在使用Entity Framework 4.2。

编辑2:这个问题不是Entity Framework / Linq to SQL的重复:Skip&Take ,它只是询问如何确保SkipTake在SQL中而不是在客户端上执行。 这个问题涉及参数化这些值。

更新:采用下面描述的lambda参数的Skip和Take扩展方法是版本6及更高版本的entity framework的一部分。 您可以通过在代码中导入System.Data.Entity命名空间来利用它们。

通常,LINQ to Entities将常量转换为常量,并将传递给查询的变量转换为参数。

问题是Skip和Take的Queryable版本接受简单的整数参数而不接受lambda表达式,因此当LINQ to Entities可以看到你传递的值时,它无法看到你使用变量传递它们的事实(换句话说,像Skip和Take这样的方法无法访问方法的闭包。

这不仅会影响LINQ to Entities中的参数化,还会影响学习的期望,即如果将变量传递给LINQ查询,则每次重新执行查询时都会使用该变量的最新值。 例如,这样的东西适用于Where但不适用于Skip或Take:

 var letter = ""; var q = from db.Beattles.Where(p => p.Name.StartsWith(letter)); letter = "p"; var beattle1 = q.First(); // Returns Paul letter = "j"; var beattle2 = q.First(); // Returns John 

请注意,相同的特性也会影响ElementAt,但LINQ to Entities目前不支持这个特性。

这是一个技巧,您可以使用它来强制Skip和Take的参数化,同时使它们的行为更像其他查询运算符:

 public static class PagingExtensions { private static readonly MethodInfo SkipMethodInfo = typeof(Queryable).GetMethod("Skip"); public static IQueryable Skip( this IQueryable source, Expression> countAccessor) { return Parameterize(SkipMethodInfo, source, countAccessor); } private static readonly MethodInfo TakeMethodInfo = typeof(Queryable).GetMethod("Take"); public static IQueryable Take( this IQueryable source, Expression> countAccessor) { return Parameterize(TakeMethodInfo, source, countAccessor); } private static IQueryable Parameterize( MethodInfo methodInfo, IQueryable source, Expression> parameterAccessor) { if (source == null) throw new ArgumentNullException("source"); if (parameterAccessor == null) throw new ArgumentNullException("parameterAccessor"); return source.Provider.CreateQuery( Expression.Call( null, methodInfo.MakeGenericMethod(new[] { typeof(TSource) }), new[] { source.Expression, parameterAccessor.Body })); } } 

上面的类定义了Skip和Take的新重载,它们期望一个lambda表达式,因此可以捕获变量。 使用这样的方法将导致变量被LINQ to Entities转换为参数:

 int x = 10; int y = 10; var query = context.Users.OrderBy(u => u.Id).Skip(() => x).Take(() => y); var result1 = query.ToList(); x = 20; var result2 = query.ToList(); 

希望这可以帮助。

可以参数化SkipTop of ObjectQuery方法。 MSDN上有一个例子。

我在自己的模型中做了类似的事情,并且sql server profiler显示了部件

 SELECT TOP (@limit) 

 WHERE [Extent1].[row_number] > @skip 

所以,是的。 可以办到。 我同意其他人的意见,这是你在这里做出的一个有价值的观察。