EF Core DbContext 快取特性實驗
| | 2 | | ![]() |
在 ASP.NET Core 使用 EF Core DbContext 的標準姿勢是在 Startup.ConfigureServices() 用 services.AddDbContextPool<MyDbContext>(options => { options.UseSqlite(...) 或是 options.UseSqlserver(...) }) 註冊成 Scoped 服務(參考:不可不知的 ASP.NET Core 依賴注入),需要存取資料庫的類別、服務則在建構式宣告 MyDbContext 參數取得 DbContext 物件。(參考:快速寫出 ASP.NET Core DI 需要的參數建構式)
由於 DbContext 的生命周期是 Scoped,在整個 HttpRequest 處理階段將共用同一顆 DbContext 物件。,例如:在 Razor Page 或 MVC Controller 同時用了 XXService 跟 YYService,XXService 與 YYService 都在建構式參數引用 MyDbContext,則 XXService 與 YYService 取得的 MyDbContext Instance 將會是同一個。這有助於維持 HttpRequest 過程資料狀態的一致性,但其中的快取特性可能會讓新手迷惑。我是在 Debug 過程遇過幾次(背景作業與網頁呼叫動作交雜,同時查看來自兩個 Scope 的 DbContext),一開始不知其然,檢查結果與資料庫老對不上,查了半天以為見鬼,摸索一陣子才搞懂怎麼一回事。
一般在 Razor Page 或 MVC Controller 我們拿到的 DbContext 都會是同一顆,為了實驗,借用昨天提到的 IServiceProvider.CreateScope 技巧,並改寫 ASP.NET Core 新增修改刪除(CRUD)介面傻瓜範例,新增一個測試用 Razor Page,在其中取得屬於不同 Scope 的兩個 DbContext 跑測試:先比對資料筆數,第一筆資料的 User 欄位;接著用第一個 DbContext 更新第一筆資料的 User 欄位,用第二個 DbContext 新增一筆記錄,做完後再查一次資料筆數及第一筆資料 User 欄位。程式如下:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CRUDExample.Models;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.DependencyInjection;
namespace CRUDExample.Pages
{
public class TestScopedDbContextModel : PageModel
{
private readonly IServiceProvider services;
private readonly JournalDbContext dbCtx1;
public TestScopedDbContextModel(IServiceProvider services, JournalDbContext dbCtx1)
{
this.services = services;
this.dbCtx1 = dbCtx1;
}
public void OnGet()
{
var rec = dbCtx1.Records.First();
using (var scope = services.CreateScope())
{
var dbCtx2 = scope.ServiceProvider.GetRequiredService<JournalDbContext>();
Debug.WriteLine($"Recourds.Count in dbCtx1 = {dbCtx1.Records.Count()}");
Debug.WriteLine($"1st Record.User in dbCtx1 = {dbCtx1.Records.Single(o => o.Id == 1).User}");
Debug.WriteLine($"Recourds.Count in dbCtx2 = {dbCtx2.Records.Count()}");
Debug.WriteLine($"1st Record.User in dbCtx2 = {dbCtx2.Records.Single(o => o.Id == 1).User}");
//dbCtx2 新增一筆
dbCtx2.Records.Add(new DailyRecord()
{
Date = DateTime.Today,
Status = StatusFlags.Normal,
User = "Jeffrey",
EventSummary = "Just say hello"
});
dbCtx2.SaveChanges();
//dbCtx1 查到的 Record User 改成 darkthread
rec.User = "darkthread";
dbCtx1.SaveChanges();
Debug.WriteLine($"Recourds.Count in dbCtx1 = {dbCtx1.Records.Count()}");
Debug.WriteLine($"1st Record.User in dbCtx1 = {dbCtx1.Records.Single(o => o.Id == 1).User}");
Debug.WriteLine($"Recourds.Count in dbCtx2 = {dbCtx2.Records.Count()}");
Debug.WriteLine($"1st Record.User in dbCtx2 = {dbCtx2.Records.Single(o => o.Id == 1).User}");
}
}
}
}
猜猜結果是什麼?
在第二次檢測,dbCtx1 有察覺到筆數增加,dbCtx2 讀到的 User 欄仍是舊值。這是因為 dbCtx2 快取了 Record 資料內容,第二次檢測時沒從 DB 讀取,故仍顯示舊值。如果第一次檢測時沒有讀取首筆 Record (下圖橘框處),則第二次檢測 dbCtx2 讀到的便是被 dbCtx1.SaveChanges() 更新過的內容(下圖箭頭所指處)。
那如果 dbCtx1 與 dbCtx2 更新同一筆 Record 會怎樣?先實驗 dbCtx1 更新 User 欄位、dbCtx2 更新 EventSummary 欄位的狀況:
如上圖所示,SaveChanges() 後,dbCtx1 認知的是只有 User 改掉的版本,dbCtx2 認知的是只有 EventSummary 異動的版本,而資料庫真實的資料狀況則是 User 與 EventSummary 都被改掉了。(註:如果 dbCtx1、dbCtx2 都更新 User 欄位,則看 SaveChanges 的時機,以後執行的為準。關於 EF Core 的更新衝突議題,下一篇再聊。)
實務上,Scoped DbContext 多半只活在一個網頁從接收請求到傳送回應的短短幾秒內,上述快取行為的影響通常不明顯,但中斷偵錯過程,即時運算或變數檢視會觀察到這些原本稍縱即逝的快取資料,請記住它與實際 DB 裡的資料可能有差距,不要像我一樣被迷惑,沒 Bug 傻傻抓半天。
Experiment of scoped EF Core DbContext caching behavior.
Comments
# by 鄉民
dbCtx 認知的是只有 EventSummary… 好像因該是 dbCtx2 認知的是只有 EventSummary…
# by Jeffrey
to 鄉民,素的,偶又漏字惹,謝謝指正。