2012-02-08 11 views
19

Entity Framework sembra sempre utilizzare le costanti nell'SQL generato per i valori forniti a Skip() e Take().Force Entity Framework utilizza la parametrizzazione SQL per un migliore riutilizzo della cache proc SQL.

Nell'esempio ultra-semplificato di seguito:

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

codice precedente genera i seguenti query 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 

Conseguente 2 piani Adhoc aggiunte alla cache SQL proc con 1 uso ciascun .

Quello che mi piacerebbe realizzare è per parametrizzare la logica Skip() e Take() quindi le seguenti query SQL vengono generati:

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 

Questo si traduce in 1 piano preparato aggiunto alla cache SQL proc con 2 usi.

Ho alcune query abbastanza complesse e sto verificando un sovraccarico significativo (sul lato SQL Server) alla prima esecuzione e un'esecuzione molto più veloce nelle esecuzioni successive (dato che può utilizzare la cache del piano). Si noti che queste query più avanzate utilizzano già sp_executesql come altri valori sono parametrizzati, quindi non sono interessato a tale aspetto.

La prima serie di query generate sopra sostanzialmente significa che qualsiasi logica di impaginazione creerà una nuova voce nella cache del piano per ogni pagina, espandendo la cache e richiedendo l'overhead di generazione del piano da sostenere per ciascuna pagina.

Posso forzare Entity Framework per parametrizzare i valori? Ho notato altri valori, ad es. nelle clausole Where, a volte parametrizza i valori e talvolta utilizza costanti.

Sono completamente fuori a pranzo? C'è qualche ragione per cui il comportamento esistente di Entity Framework è migliore del comportamento che desidero?

Edit: Nel caso in cui sia rilevante, devo dire che sto utilizzando Entity Framework 4.2.

Edit 2: Questa domanda non è un duplicato di Entity Framework/Linq to SQL: Skip & Take, che chiede semplicemente come garantire che Skip e Take eseguire in SQL invece che sul client. Questa domanda riguarda la parametrizzazione di questi valori.

+0

Questo collegamento spiega come utilizzare Linq con SQL Params sarà necessario scorrere verso il basso fino alla fine del collegamento per vedere la spiegazione e il sito di esempio LinqPad - http://www.linqpad.net/WhyLINQBeatsSQL.aspx – MethodMan

+2

Grande osservazione. Normalmente non utilizzo EF per progetti "reali", semplicemente giocando con piccole cose, e non ho mai notato questo comportamento prima. Se EF non sta parametrizzando tutto ciò che può, allora considero un enorme difetto. – CodingWithSpike

+1

ottima domanda - penseresti che fossero ottimizzati per il possibile riutilizzo dei piani – BrokenGlass

risposta

24

Aggiornamento: I metodi di estensione Skip and Take che utilizzano i parametri lambda descritti di seguito fanno parte di Entity Framework dalla versione 6 in poi. Puoi trarne vantaggio importando lo spazio dei nomi System.Data.Entity nel tuo codice.

In generale LINQ to Entities converte le costanti come costanti e le variabili passate alla query in parametri.

Il problema è che le versioni Queryable di Skip e Take accettano semplici parametri interi e non espressioni lambda, quindi mentre LINQ alle Entità può vedere i valori che si superano, non può vedere il fatto che si è utilizzata una variabile per passarli (in altre parole, metodi come Skip e Take non hanno accesso alla chiusura del metodo).

Ciò non riguarda solo la parametrizzazione in LINQ alle entità ma anche l'aspettativa appresa che se si passa una variabile a una query LINQ viene utilizzato l'ultimo valore della variabile ogni volta che si esegue nuovamente la query. Per esempio, qualcosa come questo funziona per caso, ma non per saltare o prendere:

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 

Si noti che la stessa peculiarità colpisce anche ElementAt ma questo non è attualmente supportato da LINQ to Entities.

Ecco un trucco che è possibile utilizzare per forzare la parametrizzazione di Skip e Take e allo stesso tempo li rendono comportano più come gli altri operatori di query:

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

La classe sopra definisce nuovi sovraccarichi di Skip e Prendete ciò aspettatevi un'espressione lambda e quindi potete catturare variabili. Utilizzando i metodi come questo si tradurrà nelle variabili in corso di traduzione di parametri per 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(); 

Spero che questo aiuti.

+1

Risposta sorprendente. Penso che sia bello vedere i membri del team EF coinvolgere la comunità! – GWB

+0

Sembra che i metodi di questa soluzione siano ora integrati per EF6. :) – GWB

+0

Sì :) Abbiamo aggiunto questi come metodi di estensione per IQueryable . Hai solo bisogno di mettere il namespace System.Data.Entity nella portata e dovrebbe essere in grado di scrivere Skip e Take con i parametri lambda. Aggiornerò la risposta – divega

2

I metodi Skip e Top di ObjectQuery<T> possono essere parametrizzati. C'è un esempio su MSDN.

Ho fatto una cosa simile in un modello di mio e del server SQL Profiler ha mostrato le parti

SELECT TOP (@limit) 

e

WHERE [Extent1].[row_number] > @skip 

Quindi, sì. Si può fare. E sono d'accordo con gli altri sul fatto che questa è una preziosa osservazione che hai fatto qui.

+0

Interessante che hanno aggiunto un metodo 'Top' invece di riutilizzare' Take'. Sto saltando/prendendo una proiezione anonima quindi la query non è di tipo 'ObjectQuery ' quindi non sono sicuro di poter utilizzare questo approccio. Ma questo mi dà qualcosa da investigare. – GWB

+0

Sì, questi metodi sembrano essere molto limitati in uso. Posso solo farli lavorare direttamente sul set di oggetti. Neanche sul risultato di un 'Where()', quando si esegue il cast esplicito su un oggetto ObjectQuery (i metodi _Query builder non sono supportati per LINQ alle query Entities. Per ulteriori informazioni, consultare la documentazione di Entity Framework.) –

Problemi correlati