LINQ2SQL采用開放式並發的機制來處理由於數據庫並發修改或由於讀取和修改之間存在時間差導致的數據不一致的問題。為了實現這一機制,LINQ2SQL在提交數據更改時,會先進行開放式沖突檢測,並且當發現沖突後提供了一系列解決沖突的方法。
LINQ2SQL的並發沖突處理采用optimistic concurrency(開放式並發)機制。當發生數據更新時,LINQ2SQL會檢查當前數據庫值和查詢返回的原始值是否相等,如果相等則表示數據庫值未發生變化,說明未發生沖突,可以提交更新;否則當數據庫值發生變化時,會拋出ChangeConflictException,等待用戶決定如何處理更新。
參考資料:
開放式並發概述 http://msdn.microsoft.com/zh-cn/library/bb399373.aspx
本文會用到一些名詞,為避免大家誤解,先說明如下,對於某個entity object的屬性:
1) 數據庫值:當並發沖突發生時,數據庫保存的字段值
2) 原始值:通過DataContext查詢獲得的字段值。由於從DataContext查詢到數據更新存在時間間隔,所以原始值與數據庫值可能不同。
當前值:通過entity.Property可獲取的屬性值。剛從DataContext查詢時,當前值=原始值,但在某些操作中可能修改了當前值,所以當前值和原始值可能不同。
開放式沖突檢測
可能引發Optimistic Concurrency的只有涉及Update和Delete的操作,LINQ通過執行Update或Delete的SQL語句的操作返回值(@@ROW_COUNT)來檢測是否產生沖突。
我們來執行下面的代碼(使用Northwnd數據庫):
1 var context = GenerateContext();
2 context.Log = Console.Out;
3 Customer customer = context.Customers.First();
4 customer.CompanyName = "Updating customer";
5 context.SubmitChanges();
產生的SQL語句如下,注意看Where子句和傳遞的參數
01 UPDATE [dbo].[Customers]
02 SET [CompanyName] = @p10
03 WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) A
04 ND ([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region]
05 IS NULL) AND ([PostalCode] = @p6) AND ([Country] = @p7) AND ([Phone] = @p8) AND
06 ([Fax] = @p9)
07 -- @p0: Input NChar (Size = 5; Prec = 0; Scale = 0) [ALFKI]
08 -- @p1: Input NVarChar (Size = 19; Prec = 0; Scale = 0) [Alfreds Futterkiste]
09 -- @p2: Input NVarChar (Size = 12; Prec = 0; Scale = 0) [Maria Anders]
10 -- @p3: Input NVarChar (Size = 20; Prec = 0; Scale = 0) [Sales Representative]
11 -- @p4: Input NVarChar (Size = 13; Prec = 0; Scale = 0) [Obere Str. 57]
12 -- @p5: Input NVarChar (Size = 6; Prec = 0; Scale = 0) [Berlin]
13 -- @p6: Input NVarChar (Size = 5; Prec = 0; Scale = 0) [12209]
14 -- @p7: Input NVarChar (Size = 7; Prec = 0; Scale = 0) [Germany]
15 -- @p8: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0074321]
16 -- @p9: Input NVarChar (Size = 11; Prec = 0; Scale = 0) [030-0076545]
17 -- @p10: Input NVarChar (Size = 17; Prec = 0; Scale = 0) [Updating customer]
為什麼LINQ2SQL在生成Where語句時,不但包含了CustomerID(Primary key),還包含了其他所有的屬性列呢?就是為了檢測沖突,注意看傳遞的參數——Customer object的原始值,通過原始值和數據庫值比較,判斷自上次查詢後數據庫值是否發生了更改。當發生更改時,上面的sql語句執行返回 @@ROW_COUNT=0,LINQ2SQL以此判斷是否發生了開放式並發。
上面的代碼是示例LINQ2SQL根據ColumnAttribute.UpdateCheck的設置來生成的Where子句的情況。除此之外,LINQ2SQL還可以根據標記為IsVersion和IsPrimary屬性來生成Where子句,這樣可以生成更短的SQL語句。
參考資料:
如何指定測試哪些成員是否發生並發沖突
http://msdn.microsoft.com/zh-cn/library/bb399394.aspx
如何將列表示為時間戳或版本列
http://msdn.microsoft.com/zh-cn/library/bb386957.aspx
我們來看下面的代碼,你認為在執行SubmitChanges時會產生沖突嗎?
01 var context = GenerateContext();
02 var customerA = context.Customers.First();
03 var customerB = context.Customers
04 .Where(b => b.CustomerID == customerA.CustomerID).Single();
05
06 customerA.CompanyName = "Updating customerA";
07 customerB.CompanyName = "Updating customerB";
08 context.SubmitChanges();
09
10 Customer customerC = context.Customers
11 .Where(c => c.CustomerID == customerA.CustomerID).Single();
12 Console.WriteLine(customerC.CompanyName);
不會! CompanyName被更新為”Updating customerB”。這是因為在DataContext對象中緩存了查詢返回的entity object的引用,在下一次查詢時,如果發現緩存中已經存在Primary Key與數據庫查詢返回數據的Primary Key相同的對象時,不再重新映射,而是直接返回已經緩存的對象,所以customerA和customerB是指向同一個對象的,這樣就保證了屬於同一個DataContext對象的entity object,在SubmitChanges時不會產生開放式沖突。
1 Console.WriteLine(customerA == customerB); // 輸出為True
開放式並發處理
在檢測到並發沖突後,DataContext將拋出ChangeConfilictException,通過對異常的處理,用戶可以決定如何處理並發沖突。下面代碼示例了發生Optimistic Concurrency的情況:
01 var contextA = GenerateContext();
02 // 用contextB來模擬另外一個用戶更新
03 var contextB = GenerateContext();
04
05 var customerA = contextA.Customers.First();
06 customerA.CompanyName = "Updating Value from A";
07 var customerB = contextB.Customers
08 .Where(c => c.CustomerID == customerA.CustomerID).Single();
09 customerB.CompanyName = "Updating Value from B";
10 contextB.SubmitChanges();
11
12 try
13 {
14 // 發生開放式沖突
15 contextA.SubmitChanges();
16 }
17 catch (ChangeConflictException)
18 {
19 // 在此添加代碼處理沖突
20 }
由於customerA和customerB都是指向數據庫的同一條數據,在contextB.SubmitChanges提交後,contextA.SubmitChanges就發生了沖突。
參考資料:
如何檢測和解決提交沖突
http://msdn.microsoft.com/zh-cn/library/bb387001.aspx
下面我們接著討論事務處理。在我們沒有顯式的開啟事務時,DataContext.SubmitChanges在提交時會隱式創建事務,我們看看下面這段代碼:
01 var context = GenerateContext();
02 Customer customerA = new Customer { CustomerID = "K.K" };
03 Customer customerB = new Customer { CustomerID = "K.K" };
04 context.Customers.InsertOnSubmit(customerA);
05 context.Customers.InsertOnSubmit(customerB);
06
07 try
08 {
09 context.SubmitChanges();
10 }
11 catch (Exception)
12 {
13 Console.WriteLine(context.Customers.Where(c => c.CustomerID == "K.K").Count()); // 輸出0
14 }
由於customerA和customerB的CustomerID賦值相同,造成了Primary Key的沖突,所以customerB插入失敗。但由於DataContext.SubmitChanges自動創建事務,所以customerB插入失敗造成全部操作回滾,customerA也沒有插入。
DataContext.SubmitChanges可能涉及多個entity object的更新,在實際應用中,一起提交若干記錄的更新有兩種情況:
1) 需要事務完整約束的,如銷售++,庫存--
2) 不需要事務約束的。如程序運行時把產生的日志放到cache裡,由專門負責日志持久化的Thread每隔一段時間把Cache裡的多條日志提交保存。
DataContext.SubmitChanges(ConflictMode)可通過指定ConflictMode來滿足上面的兩種情況:
ConflictMode取值 說明 FailOnFirstConflict 默認參數,一旦發生ChangeConflictException,停止SubmitChanges操作,回滾所有SubmitChanges執行的操作。滿足情況1) ContinueOnConflict 當發生ChangeConflictException時,繼續執行下一個對象的更新操作,直至所有對象都更新完畢,返回所有發生的ChangeConflictException。滿足情況2)
除了讓DataContext.SubmitChanges隱式創建事務外,我們也可以顯式使用事務來控制更新操作。下面兩段代碼分別示例了顯示事務的使用:
IDbTransaction version:
01 var context = GenerateContext();
02 Customer customerA = new Customer { CustomerID = "K.K" };
03 Customer customerB = new Customer { CustomerID = "K.K" };
04 context.Customers.InsertOnSubmit(customerA);
05 context.Customers.InsertOnSubmit(customerB);
06 context.Transaction = context.Connection.BeginTransaction();
07
08 try
09 {
10 context.SubmitChanges();
11 context.Transaction.Commit();
12 }
13 catch (Exception)
14 {
15 context.Transaction.Rollback();
16 }
System.Transactions.TransactionScope version:
01 var context = GenerateContext();
02 Customer customerA = new Customer { CustomerID = "K.K" };
03 Customer customerB = new Customer { CustomerID = "K.K" };
04 context.Customers.InsertOnSubmit(customerA);
05 context.Customers.InsertOnSubmit(customerB);
06
07 try
08 {
09 using (TransactionScope trans = new TransactionScope())
10 {
11 context.SubmitChanges();
12 trans.Complete();
13 }
14 }
15 catch (Exception) {}