筆記目錄

Skip to content

淺談 Entity Framework 中 SaveChanges() 的異常處理與狀態還原

TLDR

  • SaveChanges() 失敗後,ChangeTracker 會保留所有異動狀態,導致後續操作持續失敗。
  • 建議在 DbContext 覆寫 SaveChanges() 方法,並在捕獲 DbUpdateException 時,透過 ChangeTracker 手動還原 Entity 狀態。
  • 針對 Added 狀態的 Entity,應設為 DetachedModified 狀態則需將 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 初版文件建立。