星期日, 3月 30, 2025

[EFCore] Cache

同事遇上 EF cache 造成結果不如預期, 寫份簡易範例來記錄並介紹

C# 完整 Code

根據該筆記 [EFCore] LogTo 輸出 Linq 產生的 TSQL 語法,藉此來觀察是否發生資料庫存取,而輸出結果會稍微整理,以方便閱讀為主
using EFCoreCache.Models;
using Microsoft.EntityFrameworkCore;

namespace EFCoreCache
{
    internal class Program
    {
        static void Main(string[] args)
        {
            AdventureWorks2022DbContext dbContext = new();

            Console.WriteLine("----- Query1:第一次存取 Entity");

            Console.WriteLine($"存取前 Person Entity Cache Count:{dbContext.Person.Local.Count}");
            var query1 = dbContext.Person.SingleOrDefault(p => p.BusinessEntityID == 1);
            Console.WriteLine($"存取後 Person Entity Cache Count:{dbContext.Person.Local.Count}");
            Print(query1);

            Console.WriteLine("----- Query2:理解 Cache 行為");

            // 修正 Cache 內資料
            query1.LastName = "LastNameFromCache";

            // 直接更新資料庫內資料
            dbContext.Person
                   .Where(w => w.BusinessEntityID == 1)
                   .ExecuteUpdate(e =>
                   e.SetProperty(sod => sod.LastName, "LastNameFromDB"));

            var query2 = dbContext.Person.SingleOrDefault(p => p.BusinessEntityID == 1);
            Print(query2);

            Console.WriteLine("----- Query3:AsNoTracking()");
            var query3 = dbContext.Person.AsNoTracking().SingleOrDefault(p => p.BusinessEntityID == 1);
            Print(query3);

            Console.WriteLine("----- Query4:Find()");
            var query4 = dbContext.Person.Find(1);
            Print(query4);
        }

        private static void Print(Person p)
        {
            Console.WriteLine($"內容輸出:{p.LastName}");
        }
    }
}
後續會分別解釋每個 Query

Query1:第一次存取 Entity 

使用 Single 進行存取,目前該 entity 沒有 cache,所以會先放進 cache 內,再把 entity 往 AP 丟
Console.WriteLine($"存取前 Person Entity Cache Count:{dbContext.Person.Local.Count}");
var query1 = dbContext.Person.SingleOrDefault(p => p.BusinessEntityID == 1);
Console.WriteLine($"存取後 Person Entity Cache Count:{dbContext.Person.Local.Count}");
Print(query1);
存取前 Person Entity Cache Count:0

SELECT TOP(2) * FROM [Person].[Person] AS [p] WHERE [p].[BusinessEntityID] = 1

存取後 Person Entity Cache Count:1

內容輸出:Sanchez

Query2:理解 Cache 行為

更新 Query1 產生的 entity 資料,同時也更新資料庫內資料,藉此觀察使用 Single() 存取相同 entity 行為
// 修正 Cache 內資料
query1.LastName = "LastNameFromCache";

// 直接更新資料庫內資料
dbContext.Person
    .Where(w => w.BusinessEntityID == 1)
    .ExecuteUpdate(e =>
       e.SetProperty(p => p.LastName, "LastNameFromDB"));

var query2 = dbContext.Person.SingleOrDefault(p => p.BusinessEntityID == 1);
Print(query2);
使用 Single 存取相同的 entity,EFCore 還是會對資料庫進存取,但該資料目前已經在 cache 內,所以會把 cache 內 entity 往 AP 丟,內容輸出為 LastNameFromCache,在官方文章內有相關說明

Queries are always executed against the database even if the entities returned in the result already exist in the context.

How Queries Work - The life of a query

3. For each item in the result set

a. If the query is a tracking query, EF checks if the data represents an entity already in the change tracker for the context instance

  • If so, the existing entity is returned
  • If not, a new entity is created, change tracking is set up, and the new entity is returned

b.If the query is a no-tracking query, then a new entity is always created and returned

另外 ExecuteUpdate 是不會有 cache 的,詳見 [EFCore] ExecuteUpdate 和 ExecuteDelete

UPDATE Person.Person SET LastName = 'LastNameFromDB' WHERE BusinessEntityID = 1

SELECT TOP(2) * FROM [Person].[Person] AS [p] WHERE [p].[BusinessEntityID] = 1

內容輸出:LastNameFromCache

Query3:AsNoTracking()

使用 AsNoTracking() 就可以跳過 cache,直接存取資料庫並回傳
var query3 = dbContext.Person.AsNoTracking().SingleOrDefault(p => p.BusinessEntityID == 1);
Print(query3);
SELECT TOP(2) * FROM [Person].[Person] AS [p] WHERE [p].[BusinessEntityID] = 1

內容輸出:LastNameFromDB

Query4:Find()

當 cache 內有該 entity 時,使用 Find() 會直接回傳 cache 內 entity,不會有資料庫存取行為,沒有 TSQL 輸出,但 cache 內沒有該 entity 的話,才會去資料庫存取
var query4 = dbContext.Person.Find(1);
Print(query4);
內容輸出:LastNameFromCache
以上範例雖然是以 EFCore 9 為記錄內容,但在 EF 6.5.1 也是相同行為

沒有留言:

張貼留言