檢測並發
首先使用下面的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();
}