2016-01-17 44 views
7

在閱讀上Entity Framework performance的文章時,我看到這條信息傳來:重寫LINQ查詢表達式來啓用緩存SQL執行計劃

其次,問題[SQL Server將不能重複使用的執行計劃]首先發生,因爲在將一個int傳遞給Skip()和Take()方法時(因爲實現細節),Entity Framework無法查看它們是否傳遞了像Take(100)這樣的絕對值,或者像Take(resultsPerPage)這樣的變量,所以它不知道值是否應該被參數化。

所提出的解決辦法是改變這種風格的代碼:

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

在這種風格:

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

這使得實體框架知道這些變量的產生SQL應該參數化,這又可以使執行計劃被重用。

我們在我們的應用程序中有一些代碼以相同的方式使用變量,但是我們必須在運行時構建Expression,因爲類型事先並不知道。

這裏是它曾經的樣子:

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

的問題是,使用Expression.Constant(convertedId)導致常數來生成的SQL插入。這將導致SQL來改變你看看每一個新項目,這將停止任何執行計劃緩存:

WHERE [Extent1].[Id] = 1234 

和:

WHERE [Extent1].[Id] = 1235 

和:

WHERE [Extent1].[Id] = 1236 

那麼問題,是如何使用Expression Building來強制生成SQL的參數化?() => convertedId語法不起作用。我在下面回答了這個問題。

+0

我不明白,這裏有什麼問題? –

+0

問題是如何將上面的代碼轉換爲使用Expression.Constant時生成參數化SQL,因爲'()=> convertedId'語法不起作用。 –

+0

我已更新主帖以明確包含該問題。 –

回答

5

大量的試驗和錯誤之後,我們發現你仍然可以強制實體框架承認convertedId作爲參數,通過稍微改變我們如何通過它在:

.... 

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

這會導致生成的SQL使用對於任何給定的ID相同的參數(和代碼):

WHERE [Extent1].[Id] = @p__linq__0 

在我們處理問題的查詢需要較長的時間來生成執行計劃,所以我們看到在執行時間顯著減少訪問新的ID (從3〜4秒下降到〜300毫秒)

+0

這是正確的,skip/take示例只是使用常量值而不是參數化來突出顯示生成的SQL的基本問題的一種更簡單的方法。 –

2

讓我回顧一下。

你就像這個

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

建設Expression<Func<T, bool>>,問題是應該怎樣使用才能爲right,使EF不會把它當作一個常數。

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

顯然原始值不工作,因此該解決方案是提供一些類實例的財產。你通過使用匿名類型來解決它,但當然還有很多其他方法可以做到這一點。

例如,使用一個封閉(像它本來如果你沒有創建手動表達)

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

Tuple<T>實例(有點冗長,但消除Expression.Convert

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

+0

不錯!我非常喜歡封閉解決方案 –