2016-01-17 11 views
7

Czytając artykuł o Entity Framework performance, natknąłem się na ten kawałek informacji:Przepisywanie kwerendy LINQ Expression włączyć buforowanie SQL Execution Plan

Po drugie, problem [SQL Server nie będzie ponowne wykonanie planu ] występuje przede wszystkim dlatego, że (ze względu na szczegóły implementacji) podczas przekazywania int do metod Pomiń() i Take(), Entity Framework nie może zobaczyć, czy zostały przekazane bezwzględne wartości, takie jak Take (100) lub zmienna jak Take (resultsPerPage), więc nie wie, czy wartość powinna być sparametryzowana.

Proponowane rozwiązanie jest zmienić ten styl kodu:

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

na tym stylu:

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

który pozwala Entity Framework wiedzieć, że są one zmienne i że wygenerowany SQL powinien zostać sparametryzowany, co z kolei pozwala na ponowne wykorzystanie planu wykonania.

Mamy pewien kod w naszej aplikacji, który używa zmiennych w ten sam sposób, ale musimy budować wyrażenie w czasie wykonywania, ponieważ typ nie jest znany z góry.

Oto co kiedyś wyglądać tak:

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

Problem polega na tym, że za pomocą Expression.Constant(convertedId) powoduje stałą być włożona do wygenerowanego SQL. To powoduje, że SQL zmienić dla każdego nowego elementu spojrzeć w górę, który zatrzymuje żadnego planu wykonania buforowanie:

WHERE [Extent1].[Id] = 1234 

oraz:

WHERE [Extent1].[Id] = 1235 

oraz:

WHERE [Extent1].[Id] = 1236 

Powstaje zatem pytanie, to jest Jak używać budowania wyrażeń w taki sposób, aby wymusić parametryzację wygenerowanego kodu SQL? Składnia () => convertedId nie będzie działać. Odpowiedziałem na to poniżej.

+0

Nie rozumiem, o co tu chodzi? –

+0

Pytanie dotyczyło sposobu konwersji powyższego kodu w celu wygenerowania sparametryzowanego SQL podczas korzystania z Expression.Constant, ponieważ składnia '() => convertId' nie działa. –

+0

Zaktualizowałem główny post, aby jednoznacznie uwzględnić to pytanie. –

Odpowiedz

5

Po wielu prób i błędów, okazało się, nadal można zmusić Entity Framework rozpoznać convertedId jako parametr poprzez niewielką zmianę w jaki sposób przekazać go w:

.... 

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

co powoduje, że generowane SQL używać sam parametr (i kod) dla danego ID:

WHERE [Extent1].[Id] = @p__linq__0 

zapytanie o którym mowa, że ​​mamy do czynienia z zajmuje dużo czasu, aby wygenerować plan wykonania, więc widzieliśmy znaczący spadek w czasie wykonywania dostępu do nowych identyfikatorów (od 3 ~ 4 sekundy do ~ 300 milisekund)

+0

To prawda, przykład pominięcia/wzięcia jest prostszym sposobem podkreślenia podstawowego problemu wygenerowanego SQL przy użyciu wartości stałych zamiast parametrów sparametryzowanych. –

2

Pozwolę sobie podsumować.

Ty budujesz Expression<Func<T, bool>> jak ten

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

i pytanie jest, co powinno być wykorzystywane do right w celu uczynienia EF nie traktując go jako stała.

Widocznie prymitywny wartość jak

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

nie działa, więc rozwiązanie jest dostarczenie własność jakiejś klasy instancji. Rozwiązałeś to, używając anonimowego typu, ale oczywiście jest na to wiele innych sposobów.

Na przykład, za pomocą zamknięcia (jak to byłoby jeśli nie tworząc wyrażenie ręcznie)

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

lub Tuple<T> instancji (nieco rozwlekły, ale eliminuje Expression.Convert)

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

itp.

+0

Nice! Bardzo podoba mi się rozwiązanie zamknięcia –