EF Core - Transaction
🐧關於 Transaction
在系統開發中,最可怕的狀況不是資料存不進去,而是「資料只成功了一半」。
你可能會覺得沒什麼——多一筆少一筆資料,好像影響不大。但實際上,這種「半成品」的狀態,才是造成系統錯誤與災難的根源。
- 使用者下單成功了,庫存卻沒扣 → 超賣
- 客戶付款完成,但系統沒記帳 → 客訴、查帳風暴
- 發票開出來了,卻沒連到訂單 → 財報對不起來
這些問題不是 bug,也不是 crash,而是資料邏輯不一致,讓系統表面正常、實際混亂。這時候,就需要一種「全部成功或全部失敗」的保護機制 —— 這就是 交易(Transaction) 的存在價值。
交易就像你畫圖時的一顆橡皮擦:如果中間畫錯了,可以整張擦掉重來,而不是留下殘破不堪的塗鴉。
🌊 SaveChanges() 自動包成 Transaction
在 EF Core 中,只要你呼叫 SaveChanges() 或 SaveChangesAsync(),它就會自動包一個隱含的交易(Implicit Transaction)。這代表:
1 | using (var context = new MyDbContext()) |
- 適合簡單 CRUD
- 乾淨簡單、效能佳
- 若發生例外,自動 Rollback
🌊 使用 BeginTransaction() 控制交易範圍,可以包多次 SaveChanges()
有些情境你會需要呼叫多次 SaveChanges(),但仍然希望它們「要嘛全成,要嘛全不成」。這時就可以使用 BeginTransaction() 取得 IDbContextTransaction:
1 | using (var context = new MyDbContext()) |
- 可多次 SaveChanges()
- 手動控制 Commit / Rollback
- 單一 DbContext
但你可能會問,幹嘛不一次 SaveChanges() 就好,原因可能有
- 你需要先 SaveChanges() 才能取得資料庫生成的值(如 Identity ID)
- 你需要在中間進行額外邏輯、檢查、呼叫外部 API
- 有些資料之間有順序、依賴、條件判斷
是否「一定需要」包 transaction?取決於資料關聯的重要性與穩定性
情境 | 是否需要 Transaction | 原因 |
---|---|---|
1. 新增會員 → 寄歡迎信 | 可以不用 | 資料獨立,寄信失敗不影響主資料 |
2. 新增 User → 新增 Order | 建議使用 | 兩者有強關聯,失敗時要 Rollback |
3. 修改 A → 寫入 AuditLog | 強烈建議使用 | 若 Log 寫入失敗,主資料就不能 commit,否則會造成審計遺漏 |
4. 新增 Order → 呼叫外部金流服務 | 需要自訂補償機制或用 Transaction | 若金流失敗,Order 不想保留 |
🌊 多個 DbContext 共用同一個交易
大型系統中,往往有多個 DbContext(例如:User 資料庫、Order 資料庫),你會希望這兩邊能「一起成功或一起失敗」。這可以透過共用同一個資料庫連線與交易物件,不過條件建立在:
- 使用相同連線
- 共用交易
1 | var connection = new SqlConnection("your-connection-string"); |
但實務上,「很少」這樣做,
DbContext 拆分背後通常代表邏輯或資料來源的分離,不管是 CQRS、模組劃分、DDD 聚合分離,DbContext 拆開的目的就是要「邏輯分開」。如果還要「共用連線」,那就有點「拆了又綁」的違和感。
共用連線會帶來額外的維護負擔,你得自己手動開啟連線、關閉連線、處理例外情況,容易出現一方 Dispose 影響另一方的問題(特別是 scope 或 async context 下)
絕大多數需求下,一個 DbContext 就夠了
對同一個資料庫的操作,幾乎都可以在單一 DbContext 中完成
若有多模組需求,也可以透過 Extension / Partial Class / Repository 等方式切出邏輯,而不需強拆成多個 DbContext
🌊 使用 TransactionScope 自動處理多資源交易
TransactionScope 是 .NET 的「環境式交易管理器」,它會根據程式中所涉及的資源(SQL、File、Message Queue…):
自動升級成 分散式交易(透過 MSDTC)或維持在本地交易(例如單一 SQL Server)
只需要:
1 | using (var scope = new TransactionScope()) |
使用情境 | 原因 |
---|---|
1. 單體系統中,多個 DbContext + Dapper + ADO.NET 一起寫入資料庫 |
可用 TransactionScope 把他們包起來,避免交易手動管理太複雜 |
2. 你明確知道都連到同一個資料庫,但用的是不同資料存取技術 | 避免你要手動開交易物件 |
3. 你願意接受 MSDTC(分散式交易協調器)帶來的效能與部署複雜性 | 跨資料庫或跨主機才有意義 |
1 | using (var scope = new TransactionScope()) |
- 自動偵測參與的資源
- 自動決定 Commit / Rollback
- 必須使用 TransactionScopeAsyncFlowOption.Enabled 才能搭配 async
🌊 Transaction 實測
接下來,我們就來實測三種交易處理方式,用真實錯誤來驗證 rollback 的可靠性
🧪 SaveChanges() 測試
1 | public async Task AddItem() |
執行這段程式後,會收到錯誤訊息:
String or binary data would be truncated.
兩筆資料都沒進資料庫!這代表 EF Core 在呼叫 SaveChanges() 的時候,自動幫你包了一層交易(Transaction)。發生錯誤時,自動 Rollback。好貼心。
甚至可以用 SQL Profiler 看到,BEGIN TRANSACTION -> 然後一連串 SQL 執行 -> 接著是 ROLLBACK TRANSACTION
🧪 BeginTransaction() 測試
1 | public async Task BeginTransactionScopeTest() |
由於 DoJob1() 發生錯誤,整個交易還沒 Commit 就失敗,自然也會 Rollback。
結果兩筆資料都沒進資料庫。
SQL Profiler 證實有 BEGIN TRANSACTION -> 沒有 COMMIT -> 最後收場的是 ROLLBACK
檢查資料庫,第一筆資料並未寫入,證明有 Transaction 且 Rollback 了。由 SQL Profiler 也成功蒐集到證據,兩次 sp_executesql 前有 BEGIN TRANSACTION,之後有 ROLLBACK TRANSACTION:
🧪 TransactionScope 測試
1 | public async Task TransactionScopeInit() |
只要你沒呼叫 scope.Complete(),或者中途有一個爆了,就會自動回到初始狀態。資料沒寫入,SQL Profiler 出現 ROLLBACK。
🌊 結語
在資料寫入的世界裡,有一種義氣叫做 Transaction。
不論你是單純地用 SaveChanges(),還是自己手動開局 (BeginTransaction()),甚至升級為能跨越一切的 TransactionScope,他們的共同信念只有一句:
「我們是資料兄弟,要衝一起衝,有人滑倒就全體退場!」