﻿# 解決 LINQ to SQL 資料庫更新衝突的情形

我前陣子遇到一個偶發的錯誤狀況，就是我在我某個頁面中需要計算文件下載的次數，因此需要每次進入頁面時都要讓該筆資料的 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)](http://msdn.microsoft.com/zh-tw/library/bb399389.aspx) 文章有列出一些關於此主題的說明，建議要寫 LINQ to SQL 的開發人員務必熟讀此章節。

-   [HOW TO：偵測和解決衝突提交 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb387001.aspx)  
    描述如何偵測和解決並行存取衝突。
    
-   [HOW TO：指定何時擲回並行例外狀況 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb386936.aspx)  
    描述如何指定應該通知您發生並行存取衝突的時機。
    
-   [HOW TO：指定要測試哪些成員是否發生並行衝突 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399394.aspx)  
    描述如何將成員加上屬性 (Attribute)，以指定是否要檢查這些屬性是否發生並行存取衝突。
    
-   [HOW TO：擷取實體衝突資訊 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb387005.aspx)  
    描述如何蒐集實體衝突的相關資訊。
    
-   [HOW TO：擷取成員衝突資訊 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb386985.aspx)  
    描述如何蒐集成員衝突的相關資訊。
    
-   [HOW TO：藉由保留資料庫值來解決並行衝突 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399354.aspx)  
    描述如何以資料庫值覆寫目前值。
    
-   [HOW TO：藉由覆寫資料庫值來解決並行衝突 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399421.aspx)  
    描述如何覆寫資料庫值來保留目前值。
    
-   [HOW TO：藉由合併資料庫值來解決並行衝突 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb386918.aspx)  
    描述如何合併資料庫和目前值來解決衝突。

除了以上這些文章外，應該也要看看 [開放式並行存取概觀 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399373.aspx)，如果覺得中文看不懂也可以看看英文版的 [Optimistic Concurrency Overview (LINQ to SQL)](http://msdn.microsoft.com/en-us/library/bb399373.aspx)，因為我在看文章時有些翻譯說實在還看不太習慣。

底下是解決衝突問題的範例程式（參考  [ObjectChangeConflict.Resolve 方法 (RefreshMode)](http://msdn.microsoft.com/zh-tw/library/bb298529.aspx) 說明）

```
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，就可以避免在更新資料時發生變更衝突。只不過當衝突發生的時候，資料庫中新的值可能會被目前物件的值給蓋過去，數字會有點不精確就是了。

    ![在 LINQ to SQL Designer 中將特定的欄位的 UpdateCheck 屬性設定為 Never](https://stwillblogassets.blob.core.windows.net/files/images/external/stwillblogassets.blob.core.windows.net/9e9061b98e354931968e-image_3.png)

**相關連結**

-   [開放式並行存取概觀 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399373.aspx)
-   [HOW TO：管理變更衝突 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399389.aspx)
-   [進行和提交資料變更 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb882674.aspx)
-   [程式設計手冊 (LINQ to SQL)](http://msdn.microsoft.com/zh-tw/library/bb399408.aspx)
-   [LINQ to SQL](http://msdn.microsoft.com/zh-tw/library/bb386976.aspx)
-   [Into to LINQ to SQL Optimistic Concurrency](http://blogs.msdn.com/matt/archive/2008/05/22/into-to-linq-to-sql-optimistic-concurrency.aspx)
