檢測併發
首先使用下面的 SQL 語句查詢數據庫的產品表:
select * from products where categoryid= 1 |
查詢結果如下圖:
爲了看起來清晰,我已經事先把所有分類爲 1 產品的價格和庫存修改爲相同值了。然後執行下面的程序:
var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert .ToInt16(p.UnitsInStock - 1); ctx.SubmitChanges(); // 在這裏設斷點 |
我們使用調試方式啓動,由於設置了斷點,程序並沒有進行更新操作。此時,我們在數據庫中運行下面的語句:
update products set unitsinstock = unitsinstock - 2, unitprice= unitprice + 1 where categoryid = 1 |
然 後在繼續程序,會得到修改併發(樂觀併發衝突)的異常,提示要修改的行不存在或者已經被改動。當客戶端提交的修改對象自讀取之後已經在數據庫中發生改動, 就產生了修改併發。解決併發的包括兩步,一是查明哪些對象發生併發,二是解決併發。如果你僅僅是希望更新時不考慮併發的話可以關閉相關列的更新驗證,這樣 在這些列上發生併發就不會出現異常:
[Column (Storage="_UnitsInStock" , DbType="SmallInt" , UpdateCheck = UpdateCheck .Never)] [Column (Storage="_UnitPrice" , DbType="Money" , UpdateCheck = UpdateCheck .Never)] |
爲這兩列標註不需要進行更新檢測。假設現在產品價格和庫存分別是 27 和 32 。那麼,我們啓動程序(設置端點),然後運行 UPDATE 語句,把價格 +1 ,庫存 -2 ,然後價格和庫存分別爲 28 和 30 了,繼續程序可以發現價格和庫存分別是 28 和 31 。價格 +1 是之前更新的功勞,庫存最終是 -1 是我們程序之後更新的功勞。當在同一個字段上(庫存)發生併發衝突的時候,默認是最後的那次更新獲勝。
解決併發
如果你希望自己處理併發的話可以把前面對列的定義修改先改回來,看下面的例子:
var query = from p in ctx.Products where p.CategoryID == 1 select p; foreach (var p in query) p.UnitsInStock = Convert .ToInt16(p.UnitsInStock - 1); try { ctx.SubmitChanges(ConflictMode .ContinueOnConflict); } catch (ChangeConflictException ) { foreach (ObjectChangeConflict cc in ctx.ChangeConflicts) { Product p = (Product )cc.Object; Response.Write(p.ProductID + "<br/>" ); cc.Resolve(RefreshMode .OverwriteCurrentValues); // 放棄當前更新,所有更新以原先更新爲準 } } ctx.SubmitChanges(); |
首先可以看到,我們使用 try{}catch{} 來捕捉併發衝突的異常。在 SubmitChanges 的時候,我們選擇了 ConflictMode.ContinueOnConflict 選項。也就是說遇到併發了還是繼續。在 catch{} 中,我們從 ChangeConflicts 中獲取了併發的對象,然後經過類型轉化後輸出了產品 ID ,然後選擇的解決方案是 RefreshMode.OverwriteCurrentValues 。也就是說,放棄當前的更新,所有更新以原先更新爲準。
我們來測試一下,假設現在產品價格和庫存分別是 27 和 32 。那麼,我們啓動程序(在 ctx.SubmitChanges(ConflictMode.ContinueOnConflict) 這裏設置端點),然後運行 UPDATE 語句,把價格 +1 ,庫存 -2 ,然後價格和庫存分別爲 28 和 30 了,繼續程序可以發現價格和庫存分別是 28 和 30 。之前 SQL 語句庫存 -2 生效了,而我們程序的更新(庫存 -1 )被放棄了。在頁面上也顯示了所有分類爲 1 的產品 ID (因爲我們之前的 SQL 語句是對所有分類爲 1 的產品都進行修改的)。
然後,我們來修改一下解決併發的方式:
cc.Resolve(RefreshMode .KeepCurrentValues); // 放棄原先更新,所有更新以當前更新爲準 |
來測試一下,假設現在產品價格和庫存分別是 27 和 32 。那麼,我們啓動程序(在 ctx.SubmitChanges(ConflictMode.ContinueOnConflict) 這裏設置端點),然後運行 UPDATE 語句,把價格 +1 ,庫存 -2 ,然後價格和庫存分別爲 28 和 30 了,繼續程序可以發現價格和庫存分別是 27 和 31 。產品價格沒有變化,庫存 -1 了,都是我們程序的功勞, SQL 語句的更新被放棄了。
然後,我們再來修改一下解決併發的方式:
cc.Resolve(RefreshMode .KeepChanges); // 原先更新有效,衝突字段以當前更新爲準 |
來測試一下,假設現在產品價格和庫存分別是 27 和 32 。那麼,我們啓動程序(在 ctx.SubmitChanges(ConflictMode.ContinueOnConflict) 這裏設置端點),然後運行 UPDATE 語句,把價格 +1 ,庫存 -2 ,然後價格和庫存分別爲 28 和 30 了,繼續程序可以發現價格和庫存分別是 28 和 31 。這就是默認方式,在保持原先更新的基礎上,對於發生衝突的字段以最後更新爲準。
我們甚至還可以針對不同的字段進行不同的處理策略:
foreach (ObjectChangeConflict cc in ctx.ChangeConflicts) { Product p = (Product )cc.Object; foreach (MemberChangeConflict mc in cc.MemberConflicts) { string currVal = mc.CurrentValue.ToString(); string origVal = mc.OriginalValue.ToString(); string databaseVal = mc.DatabaseValue.ToString(); MemberInfo mi = mc.Member; string memberName = mi.Name; Response.Write(p.ProductID + " " + mi.Name + " " + currVal + " " + origVal +" " + databaseVal + "<br/>" ); if (memberName == "UnitsInStock" ) mc.Resolve(RefreshMode .KeepCurrentValues); // 放棄原先更新,所有更新以當前更新爲準 else if (memberName == "UnitPrice" ) mc.Resolve(RefreshMode .OverwriteCurrentValues); // 放棄當前更新,所有更新以原先更新爲準 else mc.Resolve(RefreshMode .KeepChanges); // 原先更新有效,衝突字段以當前更新爲準
} } |
比如上述代碼就對庫存字段作放棄原先更新處理,對價格字段作放棄當前更新處理。我們來測試一下,假設現在產品價格和庫存分別是 27 和 32 。那麼,我們啓動程序(在 ctx.SubmitChanges(ConflictMode.ContinueOnConflict) 這裏設置端點),然後運行 UPDATE 語句,把價格 +1 ,庫存 -2 ,然後價格和庫存分別爲 28 和 30 了,繼續程序可以發現價格和庫存分別爲 28 和 31 了。說明對價格的處理確實保留了原先的更新,對庫存的處理保留了當前的更新。頁面上顯示的結果如下圖:
最後,我們把提交語句修改爲:
ctx.SubmitChanges(ConflictMode .FailOnFirstConflict); |
表示第一次發生衝突的時候就不再繼續了,然後並且去除最後的 ctx.SubmitChanges(); 語句。來測試一下,在執行了 SQL 後再繼續程序可以發現界面上只輸出了數字 1 ,說明在第一條記錄失敗後,後續的併發衝突就不再處理了。
事務處理
Linq to sql 在提交更新的時候默認會創建事務,一部分修改發生錯誤的話其它修改也不會生效:
ctx.Customers.Add(new Customer { CustomerID = "abcdf" , CompanyName = "zhuye" }); ctx.Customers.Add(new Customer { CustomerID = "abcde" , CompanyName = "zhuye" }); ctx.SubmitChanges(); |
假設數據庫中已經存在顧客 ID 爲“ abcde ”的記錄,那麼第二次插入操作失敗將會導致第一次的插入操作失效。執行程序後會得到一個異常,查詢數據庫發現“ abcdf ”這個顧客也沒有插入到數據庫中。
如果每次更新後直接提交修改,那麼我們可以使用下面的方式做事務:
if (ctx.Connection != null ) ctx.Connection.Open(); DbTransaction tran = ctx.Connection.BeginTransaction(); ctx.Transaction = tran; try { CreateCustomer(new Customer { CustomerID = "abcdf" , CompanyName = "zhuye" }); CreateCustomer(new Customer { CustomerID = "abcde" , CompanyName = "zhuye" }); tran.Commit(); } catch { tran.Rollback(); }
private void CreateCustomer(Customer c) { ctx.Customers.Add(c); ctx.SubmitChanges(); } |
運行程序後發現增加顧客 abcdf 的操作並沒有成功。或者,我們還可以通過 TransactionScope 實現事務:
using (TransactionScope scope = new TransactionScope ()) { CreateCustomer(new Customer { CustomerID = "abcdf" , CompanyName = "zhuye" }); CreateCustomer(new Customer { CustomerID = "abcde" , CompanyName = "zhuye" }); scope.Complete(); } |