2016-01-17 12 views
7

Durante la lettura di un articolo su Entity Framework performance, mi sono imbattuto in questo pezzo di informazioni:Riscrittura una query LINQ Expression per consentire la memorizzazione nella cache piano di esecuzione SQL

In secondo luogo, il problema [SQL Server non riutilizzare il piano di esecuzione ] si verifica in primo luogo perché (a causa di un dettaglio di implementazione) quando si passa un int ai metodi Skip() e Take(), Entity Framework non può vedere se sono stati passati valori assoluti come Take (100) o variabile come Take (resultsPerPage), quindi non sa se il valore deve essere parametrizzato.

La soluzione proposta è quella di cambiare questo stile di codice:

var schools = db.Schools 
    .OrderBy(s => s.PostalZipCode) 
    .Skip(model.Page * model.ResultsPerPage) 
    .Take(model.ResultsPerPage) 
    .ToList(); 

per questo stile:

int resultsToSkip = model.Page * model.ResultsPerPage; 
var schools = db.Schools 
    .OrderBy(s => s.PostalZipCode) 
    .Skip(() => resultsToSkip) //must pre-calculate this value 
    .Take(() => model.ResultsPerPage) 
    .ToList(); 

che permette Entity Framework di sapere che queste sono le variabili e che l'ha generato SQL dovrebbe essere parametrizzato, che a sua volta consente il riutilizzo del piano di esecuzione.

Abbiamo un codice nella nostra applicazione che utilizza le variabili allo stesso modo, ma è necessario creare l'espressione in fase di esecuzione poiché il tipo non è noto in anticipo.

Ecco che cosa ha usato per assomigliare:

var convertedId = typeof(T).GetConvertedIdValue(id); 
var prop = GetIdProperty(typeof(T)); 

var itemParameter = Expression.Parameter(typeof(T), "item"); 
var whereExpression = Expression.Lambda<Func<T, bool>> 
    (
    Expression.Equal(
     Expression.Property(
      itemParameter, 
      prop.Name 
      ), 
     Expression.Constant(convertedId) 
     ), 
    new[] { itemParameter } 
    ); 

return Get<T>().Where(whereExpression); 

Il problema è che l'utilizzo Expression.Constant(convertedId) provoca una costante da inserire dentro al SQL generato. Questo fa sì che l'SQL per cambiare per ogni nuovo articolo che stai osservando in su, che si ferma qualsiasi piano di esecuzione nella cache:

WHERE [Extent1].[Id] = 1234 

e:

WHERE [Extent1].[Id] = 1235 

e:

WHERE [Extent1].[Id] = 1236 

La domanda allora, è Come si può utilizzare la costruzione di espressioni in modo tale da forzare la parametrizzazione dell'SQL generato? La sintassi () => convertedId non funzionerà. Ho risposto a questo di seguito.

+0

Non capisco, qual è la domanda qui? –

+0

La domanda era come convertire il codice sopra per generare un SQL parametrizzato quando si utilizza Expression.Constant, poiché la sintassi '() => convertitoId' non funziona. –

+0

Ho aggiornato il post principale per includere esplicitamente la domanda. –

risposta

5

Dopo un sacco di tentativi ed errori, abbiamo trovato è ancora possibile forzare Entity Framework di riconoscere convertedId come parametro cambiando leggermente come passiamo in:

.... 

var convObj = new 
{ 
    id = convertedId 
}; 
var rightExp = Expression.Convert(Expression.Property(Expression.Constant(convObj), "id"), convertedId.GetType()); 

var whereExpression = Expression.Lambda<Func<T, bool>> 
    (
    Expression.Equal(
     Expression.Property(
      itemParameter, 
      prop.Name 
      ), 
     rightExp 
     ), 
    new[] { itemParameter } 
    ); 

return Get<T>().Where(whereExpression); 

che fa sì che l'SQL generato per utilizzare il stesso parametro (e il codice) per ogni ID:

WHERE [Extent1].[Id] = @p__linq__0 

la query in questione che avevamo a che fare con richiede molto tempo per generare il piano di esecuzione, così abbiamo visto una notevole diminuzione del tempo di esecuzione per l'accesso ai nuovi ID (a partire dal 3 ~ 4 secondi fino a ~ 300 millisecondi)

+0

Corretto, l'esempio skip/take è solo un modo più semplice per evidenziare il problema sottostante dell'SQL generato utilizzando valori costanti invece di quelli parametrizzati. –

2

Lasciatemi ricapitolare.

State costruendo Expression<Func<T, bool>> come questo

var item = Expression.Parameter(typeof(T), "item"); 
var left = Expression.Property(item, idPropertyName); 
Expression right = ...; 
var body = Expression.Equal(left, right); 
var predicate = Expression.Lambda<Func<T, bool>>(body, item); 

e la questione è quello che dovrebbe essere utilizzato per right al fine di rendere EF non trattandolo come una costante.

valore Apparentemente primitivo come

var right = Expression.Convert(Expression.Constant(convertedId), left.Type); 

non funziona, quindi la soluzione è quella di fornire una proprietà di qualche istanza di classe. L'hai risolto usando il tipo anonimo, ma ovviamente ci sono molti altri modi per farlo.

Ad esempio, utilizzando una chiusura (come sarebbe stato se non si intende creare l'espressione manualmente)

Expression<Func<object>> closure =() => convertedId; 
var right = Expresion.Convert(closure.Body, left.Type); 

o Tuple<T> esempio (un po 'prolisso, ma elimina Expression.Convert)

var tuple = Activator.CreateInstance(
    typeof(Tuple<>).MakeGenericType(left.Type), convertedId); 
var right = Expression.Property(Expression.Constant(tuple), "Item1"); 

ecc.

+0

Bello! Mi piace molto la soluzione di chiusura –

Problemi correlati