EntityFrameworkCore 開發實踐問題及規範

ddockerman發表於2020-07-09

嚴重問題

客戶端求值

  • 如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("http://"))
    {
        url = string.Concat("http://", 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("http://"))
    {
        url = string.Concat("http://", 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 時,某些提供程式可能會引發異常或不執行任何操作。

規範參考

    資料追蹤參考規範: https://docs.microsoft.com/zh-cn/ef/core/querying/tracking

    客戶端求值參考規範:https://docs.microsoft.com/zh-cn/ef/core/querying/client-eval

    非同步查詢參考規範:https://docs.microsoft.com/zh-cn/ef/core/querying/async

    載入相關資料參考規範:https://docs.microsoft.com/zh-cn/ef/core/querying/related-data

    事務使用參考規範:https://docs.microsoft.com/zh-cn/ef/core/saving/transactions

相關文章