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

GWB*_*GWB 20 .net c# sql-server entity-framework entity-framework-4

实体框架似乎总是在生成的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();
Run Code Online (Sandbox Code Playgroud)

上面的代码生成以下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
Run Code Online (Sandbox Code Playgroud)

导致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
Run Code Online (Sandbox Code Playgroud)

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

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

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

我可以强制实体框架参数化值吗?我注意到其他值,例如在Where子句中,有时它参数化值,有时它使用常量.

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

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

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

div*_*ega 25

更新:采用下面描述的lambda参数的Skip和Take扩展方法是版本6及更高版本的实体框架的一部分.您可以通过在代码中导入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
Run Code Online (Sandbox Code Playgroud)

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

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

public static class PagingExtensions
{
    private static readonly MethodInfo SkipMethodInfo = 
        typeof(Queryable).GetMethod("Skip");

    public static IQueryable<TSource> Skip<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<int>> countAccessor)
    {
        return Parameterize(SkipMethodInfo, source, countAccessor);
    }

    private static readonly MethodInfo TakeMethodInfo = 
        typeof(Queryable).GetMethod("Take");

    public static IQueryable<TSource> Take<TSource>(
        this IQueryable<TSource> source, 
        Expression<Func<int>> countAccessor)
    {
        return Parameterize(TakeMethodInfo, source, countAccessor);
    }

    private static IQueryable<TSource> Parameterize<TSource, TParameter>(
        MethodInfo methodInfo, 
        IQueryable<TSource> source, 
        Expression<Func<TParameter>>  parameterAccessor)
    {
        if (source == null) 
            throw new ArgumentNullException("source");
        if (parameterAccessor == null) 
            throw new ArgumentNullException("parameterAccessor");
        return source.Provider.CreateQuery<TSource>(
            Expression.Call(
                null, 
                methodInfo.MakeGenericMethod(new[] { typeof(TSource) }), 
                new[] { source.Expression, parameterAccessor.Body }));
    }
}
Run Code Online (Sandbox Code Playgroud)

上面的类定义了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();
Run Code Online (Sandbox Code Playgroud)

希望这可以帮助.