我前陣子遇到一個偶發的錯誤狀況,就是我在我某個頁面中需要計算文件下載的次數,因此需要每次進入頁面時都要讓該筆資料的 num 欄位的值自動加 1,也就是每次都要更新資料庫,但是每過幾天就有可能收到幾個 System.Data.Linq.ChangeConflictException 例外狀況,錯誤訊息如下:
中文版
System.Data.Linq.ChangeConflictException: 資料列找不到,或者已變更。
英文版
System.Data.Linq.ChangeConflictException: Row not found or changed.
這原因就出在當我用 LINQ to SQL 將資料取出之後,一直到寫回資料庫的過程中,資料庫中的該筆資料發生了變更,而導致衝突狀況,我的程式碼如下:
MyTable m = (from p in db.MyTable
where p.ID.CompareTo(id) == 0
select p).FirstOrDefault();
if (m != null)
{
m.num = m.num + 1;
db.SubmitChanges();
}
雖然這段程式從取出之後就立即將更新的值送回資料庫更新,不過當網站流量大的時候這種資料更新的衝突現象似乎無法避免,我這幾天研究出 3 種可能的解決方案:
第一種:直接對資料庫下 SQL 指令(不使用 LINQ 的標準更新方式)
db.ExecuteCommand("UPDATE [dbo].[MyTable] SET num=num+1 WHERE ID = @p0", m.ID);
這應該是最簡單直覺的作法了,也不會有衝突的狀況發生,如果你只是要做簡單的「計數器」功能,建議用這一招就好了,否則請看第二種方法。
第二種:使用 LINQ to SQL 變更衝突的處理方法
在 MSDN 的 HOW TO:管理變更衝突 (LINQ to SQL) 文章有列出一些關於此主題的說明,建議要寫 LINQ to SQL 的開發人員務必熟讀此章節。
除了以上這些文章外,應該也要看看 開放式並行存取概觀 (LINQ to SQL),如果覺得中文看不懂也可以看看英文版的 Optimistic Concurrency Overview (LINQ to SQL),因為我在看文章時有些翻譯說實在還看不太習慣。
底下是解決衝突問題的範例程式(參考 ObjectChangeConflict.Resolve 方法 (RefreshMode) 說明)
try
{
db.SubmitChanges(System.Data.Linq.ConflictMode.ContinueOnConflict);
}
catch (System.Data.Linq.ChangeConflictException ex)
{
foreach (System.Data.Linq.ObjectChangeConflict occ in db.ChangeConflicts)
{
// *********************************************
// 底下三個範例是 3 選 1 喔,不要三行都寫在一起!
// **********************************************
// 採用資料庫的查詢出來的值,目前物件的值將會被資料庫最新查到的複寫
occ.Resolve(System.Data.Linq.RefreshMode.OverwriteCurrentValues);
// 採用目前物件中的值,並更新資料庫中的版本
occ.Resolve(System.Data.Linq.RefreshMode.KeepCurrentValues);
// 僅更新此物件中變更的欄位,僅將變更的欄位寫入資料庫(或稱為合併更新)
occ.Resolve(System.Data.Linq.RefreshMode.KeepChanges);
}
// 注意:解決完衝突之後要記得重新再 SubmitChanges() 一次,否則一樣不會更新資料庫
db.SubmitChanges();
}
我在驗證變更衝突的測試程式的完整原始碼如下:
db = new NEXCOMDataContext();
MyTable m = (from p in db.MyTable
where p.ID.CompareTo(MyTableID) == 0
select p).FirstOrDefault();
if (m != null)
{
// 刻意引發變更衝突
db.ExecuteCommand(@"UPDATE [dbo].[MyTable] SET num = num - 1 WHERE ID={0}", MyTableID);
m.num = m.num + 1;
try
{
db.SubmitChanges(System.Data.Linq.ConflictMode.ContinueOnConflict);
}
catch (System.Data.Linq.ChangeConflictException ex)
{
Response.Write(String.Format("<xmp>ChangeConflictException = {0}</xmp>", ex.Message));
foreach (System.Data.Linq.ObjectChangeConflict occ in db.ChangeConflicts)
{
// 採用目前物件中的值,並更新資料庫中的版本
//occ.Resolve(System.Data.Linq.RefreshMode.KeepCurrentValues);
// 採用資料庫的查詢出來的值,目前物件的值將會被資料庫最新查到的複寫
//occ.Resolve(System.Data.Linq.RefreshMode.OverwriteCurrentValues);
// 僅更新此物件中變更的欄位,僅將變更的欄位寫入資料庫(合併)
occ.Resolve(System.Data.Linq.RefreshMode.KeepChanges);
}
// 注意:解決完衝突之後要記得重新再 SubmitChanges() 一次,否則一樣不會更新資料庫
db.SubmitChanges();
}
}
Response.End();
變更衝突是開發資料庫應用經常會發生的問題,觀念務必要清楚明瞭,下次遇到問題的時候才能快速反應出最正確的解決方案。
第三種:採用封「閉式並行存取控制(Pessimistic Concurrency Control)」,或也有人稱為「悲觀同步存取控制」
只要在 LINQ to SQL Designer 中將特定的欄位的 UpdateCheck 屬性設定為 Never,就可以避免在更新資料時發生變更衝突。只不過當衝突發生的時候,資料庫中新的值可能會被目前物件的值給蓋過去,數字會有點不精確就是了。
相關連結