在《在Linq to Sql中管理並發更新時的沖突(2):引發更新沖突》一文中 ,我們描述了Linq to Sql檢測在更新時是否產生了沖突的基本方法:將該記錄 每個字段原來的值和更新時的值進行對比,如果稍有不同則意味著記錄被修改過 ,因此產生了更新沖突。不過您是否有這樣的感覺,這種方法實在累贅了一些? 如果一個表中有數十個字段,那麼更新就必須完整地檢測一遍(不過我會在今後 的文章中提到這方面的控制)。再者,如果其中某一個字段儲存了洋洋灑灑上萬 字的文章,那麼在驗證時僅僅是將它從Web服務器發送到數據庫服務器就需要耗 費可觀的帶寬與時間,這是不是顯得有些“得不償失”呢?
因此Linq to Sql提供了另外一種檢測並發更新沖突的方式:使用記錄的時間戳 。這並不是Linq to Sql特有的功能,如果您了解其他的ORM框架的話,就會發現 諸如Hibernate也提供了類似的機制——自然,在使用上不會像Linq to Sql那樣方便。
在Sql Server中設計數據表時,我們可以使用一個特 殊的數據類型:timestamp。請不要將它與SQL-2003標准中的timestamp類型混淆 起來,那裡的timestamp和Sql Server中的datetime比較相似(Oracle中 timestamp的概念符合SQL-2003標准,而MySql中timestamp的概念與Sql Server 相同),而Sql Server中的timestamp與SQL-2003標准中的rowversion類型對應 。Sql Server中的timestamp類型和binary(8)在存儲上非常類似(不過nullable 的timestamp和nvarchar(8)類似),從類型名稱上我們就可以看出,這是一個 “時間戳”字段:當數據表中的某一條記錄被添加或者修改之後, Sql Server會自動向類型為timestamp的字段寫入當前時間。換句話說,只要在 更新時發現該字段的值沒有被修改過,就表明沒有產生並發沖突。
我們 還是通過一個例子來體驗一下吧。
如上圖。我們定義了一個新的數據表,其中有個 record_version字段為timestamp類型,這就是記錄的時間戳(record_version 這個字段名似乎有點不太“雅觀”,我覺得我們不會去主動使用它, 所以問題不大——當然一些靜態檢查工具可不這麼認為:))。有了記 錄的時間戳,我們就可以在檢測更新沖突時獲得更好的性能了。
try
{
LinqToSqlDemoDataContext dataContext = new LinqToSqlDemoDataContext();
Order order = dataContext.Orders.Single(o => o.OrderID == 1);
order.Name = "New Order Name";
dataContext.Log = Console.Out;
// 在下面的語句上設置一個斷點
dataContext.SubmitChanges();
}
catch (ChangeConflictException e)
{
Console.WriteLine (e.Message);
}
Console.ReadLine();
在最後的語句上設 置斷點,並且在程序運行至斷點後去數據庫裡對OrderID為1的紀錄作任意更新。 然後按F5繼續運行:
UPDATE [dbo].[Order]
SET [Name] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)
SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1].[OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 14; Prec = 0; Scale = 0) [New Order Name]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
上面代碼中的UPDATE語句相信大家都很清楚其含義。不 過這裡可能還需要解釋其他兩個問題:
首先是那句SELECT語句。如果您 去閱讀自動生成的Object Model的代碼時就會發現,record_version屬性上有一 個ColumnAttribute標記(假設您使用了Attribute Based Mapping Source), 其AutoSync屬性為Always,因此在任何操作之後,Linq to Sql都會補充一句 SELECT語句,以此獲得新的數據並修改DataContext中的特定對象。其次,由於 timestamp類型的數據在記錄被修改時就會設置,因此在更新時其他紀錄的值與 之前相同,也會引發更新沖突,這一點和基於字段值比較的前一種方法是不同的 。
那麼,我們一直說出現了“並發更新沖突”,那麼發生沖 突後又會出現什麼問題呢?我們來看一個略有些復雜的示例:
try
{
LinqToSqlDemoDataContext dataContext = new LinqToSqlDemoDataContext();
Order order1 = dataContext.Orders.Single(o => o.OrderID == 1);
Order order2 = dataContext.Orders.Single(o => o.OrderID == 2);
Order order3 = dataContext.Orders.Single(o => o.OrderID == 3);
Console.WriteLine("Order 1:" + order1.Introduction);
Console.WriteLine("Order 2:" + order2.Introduction);
Console.WriteLine("Order 3:" + order3.Introduction);
Console.WriteLine();
order1.Introduction = "Order 1 modified.";
order2.Introduction = "Order 2 modified.";
order3.Introduction = "Order 3 modified.";
dataContext.Log = Console.Out;
// 在下面的語句上設置一個斷點
dataContext.SubmitChanges();
}
catch (ChangeConflictException e)
{
Console.WriteLine("- --------- ' + e.Message + ' ----------");
}
LinqToSqlDemoDataContext db = new LinqToSqlDemoDataContext();
Order o1 = db.Orders.Single(o => o.OrderID == 1);
Order o2 = db.Orders.Single(o => o.OrderID == 2);
Order o3 = db.Orders.Single(o => o.OrderID == 3);
Console.WriteLine ("Order 1: " + o1.Introduction);
Console.WriteLine ("Order 2:" + o2.Introduction);
Console.WriteLine ("Order 3:" + o3.Introduction);
Console.ReadLine ();
假設我們的數據表裡有以下三條記錄:
OrderID
Name
Introduction
當程序進入到 SubmitChanges語句的斷點時,我們去數據庫中運行以下代碼,以修改OrderID為 2的記錄。
UPDATE Order SET OrderID = "New Order 2" WHERE OrderID = 2
繼續運行程序,最終控制台中會打印出以下信息:
Order 1: This is order 1
Order 2: This is order 2
Order 3: This is order 3
UPDATE [dbo].[Order]
SET [Introduction] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)
SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1]. [OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 26; Prec = 0; Scale = 0) [Order 1 modified.]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [1]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
UPDATE [dbo].[Order]
SET [Introduction] = @p2
WHERE ([OrderID] = @p0) AND ([record_version] = @p1)
SELECT [t1].[record_version]
FROM [dbo].[Order] AS [t1]
WHERE ((@@ROWCOUNT) > 0) AND ([t1]. [OrderID] = @p3)
-- @p0: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- @p1: Input Timestamp (Size = 8; Prec = 0; Scale = 0) [SqlBinary(8)]
-- @p2: Input NVarChar (Size = 26; Prec = 0; Scale = 0) [Order 2 modified.]
-- @p3: Input Int (Size = 0; Prec = 0; Scale = 0) [2]
-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.21022.8
---------- 1 of 2 updates failed. ----------
Order 1: This is order 1
Order 2: This is order 2
Order 3: This is order 3
首先我們分別打印出三個 Video對象的Introduction並將它們修改為新的值。在SubmitChanges方法調用之 前,數據庫中ID為2的記錄已經被修改過了,因此在第一組UPDATE+SELECT調用成 功之後——請注意,這是一次調用,Linq to Sql每次更新一條記錄 ——在更新第二條記錄之後發現了並發沖突。於是拋出異常(請注意 異常的Message表示“兩次更新其中有一次失敗了”),第三條記錄 也不會再更新了。在沖突發生之後,ID為2和紀錄自然沒有被修改(WHERE條件不 成立),但是第一條記錄呢?從try...catch塊之後的操作中看,ID為1的記錄也 沒有被更新。
也就是說,第一次更新被回滾了。這自然是事務的作用。 在調用(默認的)SubmitChanges方法時,Linq to Sql會把所有的更新放在同一 個事務中,因此它們“共同進退”。但是由於業務需求不同,有時候 我們不希望某條記錄的沖突導致了所有更新失敗。自然,Linq to Sql也提供了 這個方面的控制。在下一篇文章中,我們就來看一下Linq to Sql中與樂觀並發 控制有關的事務問題,以及出現並發沖突之後的解決方式。