在多使用者併發環境下,資料一致性是核心挑戰。Entity Framework 採用樂觀並發模式,預設不檢查並發衝突,僅在開發者明確標記需並發控制的屬性時,才於更新/刪除的 T-SQL WHERE 子句中加入原始值比對。本文詳細說明如何設定並發模式、攔截衝突異常,並採用適當策略解決。
在 EDM 設計師(或手動修改 CSDL)中,將需參與並發檢查的屬性(如 Color、Timestamp、RowVersion)的 ConcurrencyMode 設為 Fixed:

未設定並發檢查(預設):
UPDATE [SalesLT].[Product]
SET [Color] = @0, [StandardCost] = @1, [ListPrice] = @2
WHERE ([ProductID] = @3)
-- 僅主鍵比對,最後寫入者獲勝
設定 Color 為 Fixed 後:
UPDATE [SalesLT].[Product]
SET [Color] = @0, [StandardCost] = @1, [ListPrice] = @2
WHERE (([ProductID] = @3) AND ([Color] = @4))
-- WHERE 子句多了原始 Color 值比對,若資料庫值已被他人修改,影響筆數為 0,拋出異常
當 SaveChanges() 偵測到影響筆數為 0,會拋出 OptimisticConcurrencyException。透過 ObjectContext.Refresh() 可重新同步實體狀態:
public void UpdateProductWithConcurrencyHandling()
{
// Context1 讀取並修改
var product1 = context.Product.FirstOrDefault(p => p.ProductID == 1004);
if (product1 != null)
{
product1.Color = "Black";
product1.StandardCost = 20;
product1.ListPrice = 25;
}
// 模擬另一個 Context 同步修改同一筆資料
using (var context2 = new AdventureWorksLTEntities())
{
var product2 = context2.Product.FirstOrDefault(p => p.ProductID == 1004);
if (product2 != null)
{
product2.Color = "Blue";
product2.StandardCost = 18;
product2.ListPrice = 30;
}
// Context1 先存檔成功
context.SaveChanges();
try
{
// Context2 存檔將拋出 OptimisticConcurrencyException
context2.SaveChanges();
}
catch (OptimisticConcurrencyException ex)
{
Console.WriteLine("偵測到並發衝突:" + ex.Message);
// 策略一:ClientWins(以用戶端修改值覆蓋資料庫,最後寫入者獲勝)
context2.Refresh(RefreshMode.ClientWins, product2);
context2.SaveChanges(); // 再次嘗試存檔
// 策略二:StoreWins(放棄用戶端修改,重新載入資料庫最新值)
// context2.Refresh(RefreshMode.StoreWins, product2);
// 此時 product2 屬性值已同步為資料庫最新值,UI 可提示使用者重新編輯
}
}
}
| 模式 | 行為 | 適用場景 |
|---|---|---|
RefreshMode.ClientWins |
保留記憶體中實體的修改值,將原始值更新為資料庫最新值,再次 SaveChanges 會以用戶端值覆蓋資料庫。 |
「最後寫入者獲勝」邏輯,適合低衝突、用戶可接受覆蓋他人修改的簡單應用。 |
RefreshMode.StoreWins |
捨棄記憶體中實體的修改,完全重新載入資料庫最新值覆蓋原始值與目前值。 | 需提示使用者「資料已被他人修改,請重新編輯」的嚴謹業務流程。 |
單純用 ClientWins 或 StoreWins 都可能導致資料遺失。最佳實務建議:
OptimisticConcurrencyException。StoreWins 重新載入最新資料。此流程確保資料完整性,同時尊重使用者操作意圖,是企業級應用處理並發的標準模式。