EntityFrameworkCore 開發實踐問題及規範
- 2020 年 7 月 9 日
- 筆記
- Asp.Net Core, ef core, orm
嚴重問題
客戶端求值
- 如where條件包含的GetValueOrDefault()不能被翻譯成sql語句
- 不規範程式碼段例子
public async Task<List<Person>> GetPersonsAsync()
{
var results = await _context.Person
.Where(p => p.State.GetValueOrDefault() == 1)
.ToListAsync()
return results;
}
客戶端與伺服器評估
作為一般規則,Entity Framework Core 會嘗試儘可能全面地評估伺服器上的查詢。 EF Core 將查詢的一部分轉換為可在客戶端評估的參數。 系統將查詢的其餘部分(及生成的參數)提供給資料庫提供程式,以確定要在伺服器上評估的等效資料庫查詢。 EF Core 支援在頂級投影中進行部分客戶端評估(基本上為最後一次調用 Select())。 如果查詢中的頂級投影無法轉換為伺服器,EF Core 將從伺服器中提取任何所需的數據,並在客戶端上評估查詢的其餘部分。 如果 EF Core 在頂級投影之外的任何位置檢測到不能轉換為伺服器的表達式,則會引發運行時異常。 請參閱查詢工作原理,了解 EF Core 如何確定哪些表達式無法轉換為伺服器。
在 3.0 版之前,Entity Framework Core 支援在查詢中的任何位置進行客戶端評估。
頂級投影中的客戶端評估
在下面的示例中,一個輔助方法用於標準化從 SQL Server 資料庫中返回的部落格的 URL。 由於 SQL Server 提供程式不了解此方法的實現方式,因此無法將其轉換為 SQL。 查詢的所有其餘部分是在資料庫中評估的,但通過此方法傳遞返回的 URL 卻是在客戶端上完成。
var blogs = context.Blogs
.OrderByDescending(blog => blog.Rating)
.Select(blog => new
{
Id = blog.BlogId,
Url = StandardizeUrl(blog.Url)
})
.ToList();
public static string StandardizeUrl(string url)
{
url = url.ToLower();
if (!url.StartsWith("//"))
{
url = string.Concat("//", url);
}
return url;
}
不支援的客戶端評估
儘管客戶端評估非常有用,但有時會減弱性能。 請看以下查詢,其中的 where 篩選器現已使用輔助方法。 由於資料庫中不能應用篩選器,因此需要將所有數據提取到記憶體中,以便在客戶端上應用篩選器。 根據伺服器上的篩選器和數據量,客戶端評估可能會減弱性能。 因此 Entity Framework Core 會阻止此類客戶端評估,並引發運行時異常。
var blogs = context.Blogs
.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
.ToList();
顯式客戶端評估
在某些情況下,可能需要以顯式方式強制進行客戶端評估,如下所示
由於數據量小,因此在進行客戶端評估時才不會大幅減弱性能。
所用的 LINQ 運算符不會進行任何伺服器端轉換。
在這種情況下,通過調用 AsEnumerable 或 ToList 等方法(若為非同步,則調用 AsAsyncEnumerable 或 ToListAsync),以顯式方式選擇進行客戶端評估。 使用 AsEnumerable 將對結果進行流式傳輸,但使用 ToList 將通過創建列表來進行緩衝,因此也會佔用額外的記憶體。 但如果枚舉多次,則將結果存儲到列表中可以帶來更大的幫助,因為只有一個對資料庫的查詢。 根據具體的使用情況,你應該評估哪種方法更適合。
var blogs = context.Blogs
.AsEnumerable()
.Where(blog => StandardizeUrl(blog.Url).Contains("dotnet"))
.ToList();
客戶端評估中潛在的記憶體泄漏
由於查詢轉換和編譯的開銷高昂,因此 EF Core 會快取已編譯的查詢計劃。 快取的委託在對頂級投影進行客戶端評估時可能會使用客戶端程式碼。 EF Core 為樹型結構中客戶端評估的部分生成參數,並通過替換參數值重用查詢計劃。 但表達式樹中的某些常數無法轉換為參數。 如果快取的委託包含此類常數,則無法將這些對象垃圾回收,因為它們仍被引用。 如果此類對象包含 DbContext 或其中的其他服務,則會導致應用的記憶體使用量逐漸增多。 此行為通常是記憶體泄漏的標誌。 只要遇到的常數為不能使用當前資料庫提供程式映射的類型,EF Core 就會引發異常。 常見原因及其解決方案如下所示:
使用實例方法:在客戶端投影中使用實例方法時,表達式樹包含實例的常數。 如果你的方法不使用該實例中的任何數據,請考慮將該方法設為靜態方法。 如果需要方法主體中的實例數據,則將特定數據作為實參傳遞給方法。
將常數實參傳遞給方法:這種情況通常是由於在客戶端方法的實參中使用 this 引起的。 請考慮將實參拆分為多個標量實參,可由資料庫提供程式進行映射。
其他常數:如果在任何其他情況下都出現常數,則可以評估在處理過程中是否需要該常數。 如果必須具有常數,或者如果無法使用上述情況中的解決方案,則創建本地變數來存儲值,並在查詢中使用局部變數。 EF Core 會將局部變數轉換為形參。
建議解決
無用追蹤
- 無須追蹤的數據沒有加AsNoTracking
跟蹤與非跟蹤查詢
跟蹤行為決定了 Entity Framework Core 是否將有關實體實例的資訊保留在其更改跟蹤器中。 如果已跟蹤某個實體,則該實體中檢測到的任何更改都會在 SaveChanges() 期間永久保存到資料庫。 EF Core 還將修復跟蹤查詢結果中的實體與更改跟蹤器中的實體之間的導航屬性。
從不跟蹤無鍵實體類型。 無論在何處提到實體類型,它都是指定義了鍵的實體類型。
跟蹤查詢
返回實體類型的查詢是默認會被跟蹤的。 這表示可以更改這些實體實例,然後通過 SaveChanges() 持久化這些更改。 在以下示例中,將檢測到對部落格評分所做的更改,並在 SaveChanges() 期間將這些更改持久化到資料庫中。
var blog = context.Blogs.SingleOrDefault(b => b.BlogId == 1);
blog.Rating = 5;
context.SaveChanges();
非跟蹤查詢
在只讀方案中使用結果時,非跟蹤查詢十分有用。 可以更快速地執行非跟蹤查詢,因為無需設置更改跟蹤資訊。 如果不需要更新從資料庫中檢索到的實體,則應使用非跟蹤查詢。 可以將單個查詢替換為非跟蹤查詢。
var blogs = context.Blogs
.AsNoTracking()
.ToList();
還可以在上下文實例級別更改默認跟蹤行為:
context.ChangeTracker.QueryTrackingBehavior = QueryTrackingBehavior.NoTracking;
var blogs = context.Blogs.ToList();
標識解析
由於跟蹤查詢使用更改跟蹤器,因此 EF Core 將在跟蹤查詢中執行標識解析。 當具體化實體時,如果 EF Core 已被跟蹤,則會從更改跟蹤器返回相同的實體實例。 如果結果中多次包含相同的實體,則每次會返回相同的實例。 非跟蹤查詢不會使用更改跟蹤器,也不會執行標識解析。 因此會返回實體的新實例,即使結果中多次包含相同的實體也是如此。 此行為與 EF Core 3.0 之前的版本中的行為有所不同,請參閱早期版本。
跟蹤和自定義投影
即使查詢的結果類型不是實體類型,默認情況下 EF Core 也會跟蹤結果中包含的實體類型。 在以下返回匿名類型的查詢中,結果集中的 Blog 實例會被跟蹤。
var blog = context.Blogs
.Select(b =>
new
{
Blog = b,
PostCount = b.Posts.Count()
});
如果結果集包含來自 LINQ 組合的實體類型,EF Core 將跟蹤它們。
var blog = context.Blogs
.Select(b =>
new
{
Blog = b,
Post = b.Posts.OrderBy(p => p.Rating).LastOrDefault()
});
如果結果集不包含任何實體類型,則不會執行跟蹤。 在以下查詢中,我們返回匿名類型(具有實體中的某些值,但沒有實際實體類型的實例)。 查詢中沒有任何被跟蹤的實體。
var blog = context.Blogs
.Select(b =>
new
{
Id = b.BlogId,
Url = b.Url
});
EF Core 支援執行頂級投影中的客戶端評估。 如果 EF Core 具體化實體實例以進行客戶端評估,則會跟蹤該實體實例。 此處,由於我們要將 blog 實體傳遞到客戶端方法 StandardizeURL,因此 EF Core 也會跟蹤部落格實例。
var blogs = context.Blogs
.OrderByDescending(blog => blog.Rating)
.Select(blog => new
{
Id = blog.BlogId,
Url = StandardizeUrl(blog)
})
.ToList();
public static string StandardizeUrl(Blog blog)
{
var url = blog.Url.ToLower();
if (!url.StartsWith("//"))
{
url = string.Concat("//", url);
}
return url;
}
EF Core 不會跟蹤結果中包含的無鍵實體實例。 但 EF Core 會根據上述規則跟蹤帶有鍵的實體類型的所有其他實例。
在 EF Core 3.0 之前,某些上述規則的工作方式有所不同。 有關詳細資訊,請參閱早期版本。
沒有使用非同步方法
- 沒有優先使用非同步方法
- 不規範程式碼段例子
public async Task<int> AddPersons(IEnumerable<Person> persons)
{
this._context.Person.AddRange(persons);
return await this._context.SaveChangesAsync();
}
非同步查詢
當在資料庫中執行查詢時,非同步查詢可避免阻止執行緒。 非同步查詢對於在胖客戶端應用程式中保持響應式 UI 非常重要。 非同步查詢還可以增加 Web 應用程式中的吞吐量,即通過釋放執行緒,以處理 Web 應用程式中的其他請求。 有關詳細資訊,請參閱使用 C# 非同步編程。
EF Core 不支援在同一上下文實例上運行多個並行操作。 應始終等待操作完成,然後再開始下一個操作。 這通常是通過在每個非同步操作上使用 await 關鍵字完成的。
Entity Framework Core 提供一組類似於 LINQ 方法的非同步擴展方法,用於執行查詢並返回結果。 示例包括 ToListAsync()、ToArrayAsync()、SingleAsync()。 某些 LINQ 運算符(如 Where(…) 或 OrderBy(…))沒有對應的非同步版本,因為這些方法僅用於構建 LINQ 表達式樹,而不會導致在資料庫中執行查詢。
EF Core 非同步擴展方法在 Microsoft.EntityFrameworkCore 命名空間中定義 。 必須導入此命名空間才能使這些方法可用。
public async Task<List<Blog>> GetBlogsAsync()
{
using (var context = new BloggingContext())
{
return await context.Blogs.ToListAsync();
}
}
事務濫用
- 沒必要使用事務的場景使用事務
- 不規範程式碼段例子
public async Task<bool> UpdatePersonInfo(List<Person> persons, List<Address> addresses)
{
using (var transaction = _dbContext.Database.BeginTransaction(IsolationLevel.ReadCommitted))
{
try
{
_dbContext.Person.UpdateRange(persons);
await _dbContext.SaveChangesAsync();
_dbContext.Address.UpdateRange(addresses);
await _dbContext.SaveChangesAsync();
transaction.Commit();
return true;
}
catch (Exception ex)
{
transaction.Rollback();
throw new InternalServerErrorException($"更新失敗,ErrorMessage:{ex.Message}\r\nInnerException:{ex.InnerException}", ex);
}
}
}
使用事務
事務允許以原子方式處理多個資料庫操作。 如果已提交事務,則所有操作都會成功應用到資料庫。 如果已回滾事務,則所有操作都不會應用到資料庫。
默認事務行為
默認情況下,如果資料庫提供程式支援事務,則會在單次調用 SaveChanges() 時將所有更改都將應用到事務中。 如果其中有任何更改失敗,則會回滾事務且所有更改都不會應用到資料庫。 這意味著,SaveChanges() 可保證要麼完全成功,要麼在出現錯誤時不修改資料庫。
對於大多數應用程式,此默認行為已足夠。 除非應用程式確有需求,否則不應手動控制事務。
控制事務
可以使用 DbContext.Database API 開始、提交和回滾事務。 以下示例顯示了在單個事務中執行的兩個 SaveChanges() 操作以及 一個LINQ 查詢。
並非所有資料庫提供程式都支援事務。 調用事務 API 時,某些提供程式可能會引發異常或不執行任何操作。
規範參考
數據追蹤參考規範: //docs.microsoft.com/zh-cn/ef/core/querying/tracking
客戶端求值參考規範://docs.microsoft.com/zh-cn/ef/core/querying/client-eval
非同步查詢參考規範://docs.microsoft.com/zh-cn/ef/core/querying/async
載入相關數據參考規範://docs.microsoft.com/zh-cn/ef/core/querying/related-data
事務使用參考規範://docs.microsoft.com/zh-cn/ef/core/saving/transactions