淺談 Entity Framework 中 SaveChanges() 的異常處理與狀態還原
TLDR
SaveChanges()失敗後,ChangeTracker會保留所有異動狀態,導致後續操作持續失敗。- 建議在
DbContext覆寫SaveChanges()方法,並在捕獲DbUpdateException時,透過ChangeTracker手動還原 Entity 狀態。 - 針對
Added狀態的 Entity,應設為Detached;Modified狀態則需將CurrentValues還原為OriginalValues並設為Unchanged。 - 若 Entity 結構涉及外鍵(Foreign Key)或複雜導覽屬性,還原
EntityState可能導致快取與資料不一致,不建議在複雜關聯情境下使用此還原機制。 - 應區分前端是否可見錯誤訊息,選擇將詳細錯誤記錄於 Log 或透過自訂 Exception 重新拋出。
Entity Framework 的 Exception 處理
在開發過程中,處理 SaveChanges() 拋出的例外是確保系統穩定性的關鍵。常見的例外類型包括 DbUpdateException(儲存失敗)與 DbUpdateConcurrencyException(並發衝突)。
錯誤訊息處理建議
什麼情況下會遇到這個問題:當系統拋出底層資料庫錯誤,且開發者需要同時兼顧 Log 的詳細度與前端的安全性時。
- 前端可見原始錯誤時:應在寫入 Log 時,從
InnerException提取完整錯誤資訊,並對前端隱藏細節。 - 前端不可見錯誤時:建議在
DbContext覆寫SaveChanges(),捕獲例外後重新拋出一個包含完整錯誤訊息的 Exception,以利權責劃分。
SaveChanges() 失敗時的狀態還原
什麼情況下會遇到這個問題:當 SaveChanges() 執行失敗後,ChangeTracker 仍保留失敗的異動狀態,導致後續正常的寫入操作會連帶包含該筆失敗資料,進而引發連鎖失敗。
若希望在發生錯誤時忽略該次異動,可透過以下實作還原 ChangeTracker 的狀態:
csharp
private static void ResetEntityState(EntityEntry entry) {
switch (entry.State) {
case EntityState.Added:
entry.State = EntityState.Detached;
break;
case EntityState.Modified:
entry.CurrentValues.SetValues(entry.OriginalValues);
entry.State = EntityState.Unchanged;
break;
case EntityState.Deleted:
// 針對關聯資料,建議設為 Detached 以避免導覽屬性同步異常
entry.State = entry.Entity is Dictionary<string, object>
? EntityState.Detached
: EntityState.Unchanged;
break;
}
}WARNING
SaveChanges() 失敗後還原 Entity State 的方法僅適用於不含外鍵的簡單 Entity 結構。若涉及複雜的導覽屬性,還原狀態可能導致 DbContext 快取與資料庫內容不一致。
測試結果與限制
什麼情況下會遇到這個問題:當 Entity 之間存在外鍵關聯,且透過導覽屬性進行 Add() 或 Remove() 操作時。
實驗結果顯示,當嘗試還原 EntityState.Deleted 的關聯資料時,若將狀態設為 Unchanged,會導致導覽屬性無法正確從資料庫重新載入(因為 DbContext 已快取了該物件)。若設為 Detached,雖然導覽屬性可恢復,但整體狀態管理仍存在風險。
WARNING
使用 DbSet.Add() 加入與已查詢資料具有相同 PK 的 Entity 時,會拋出 InvalidOperationException。由於此例外發生在 Add() 階段而非 SaveChanges(),因此上述的錯誤處理機制無法攔截此類錯誤。
異動歷程
- 2024-08-17 初版文件建立。
