關於Ling to SQL如何處理交易,一直有個疑問 -- 當多筆資料的更新動作必須包成Transaction時,在Linq to SQL中應如何處理?

花了點時間研究,心得如下:

  1. 當連續進行多筆資料更新,再一次DataContext.SubmitChanges();,預設Linq to SQL會自動將這些INSERT/UPDATE/DELETE包成一個Transaction。例如:
    var order1 = (
        from o in db.Orders
        where o.OrderID == 10248
        select o).First();
    order1.ShipPostalCode = DateTime.Now.ToString("HHmmss");
    var order2 = (
        from o in db.Orders
        where o.OrderID == 10249
        select o).First();
    order2.ShipPostalCode = "123456789ABCDEF";
    db.SubmitChanges();
    第二筆訂單更新時,故意將郵遞區號設成123456789ABCDEF,會超出VARCHAR(10)的長度限而出錯。實際測試,可以發現連第一筆更新也不會寫入資料庫,證明兩筆UPDATE動作被自動包成Transaction。用SQL Profiler可以看到更明確的證據:
     
  2. 如果你希望能精確控制Transaction的Submit、Rollback,例如: 兩個DataContext共用一條Connection,可用類似以下的寫法:
    SqlConnection cn = new SqlConnection(connString);
    //兩個DataContext共用同一條Connection
    Northwind dbA = new Northwind(cn);
    Northwind dbB = new Northwind(cn);
     
    //設定DataContext.Transaction
    cn.Open();
    DbTransaction trn = cn.BeginTransaction();
    dbA.Transaction = trn;
    dbB.Transaction = trn;
     
    var order1 = (
        from o in dbA.Orders
        where o.OrderID == 10248
        select o).First();
    order1.ShipPostalCode = DateTime.Now.ToString("HHmmss");
     
    var order2 = (
        from o in dbB.Orders
        where o.OrderID == 10249
        select o).First();
    order2.ShipPostalCode = DateTime.Now.ToString("HHmmss");
     
    //若這裡才設定Transaction,則SELECT時不會包Transaction
    //dbA.Transaction = trn;
    //dbB.Transaction = trn;
     
    dbA.SubmitChanges();
    dbB.SubmitChanges();
     
    //故意Rollback(白忙一場XD)
    trn.Rollback();
    cn.Close();
  3. 除了以上的兩種Transaction做法,如果要做跨資料庫的Distributed Transaction,就要靠好用的TransactionScope
    Northwind dbA = new Northwind(connString);
    Northwind dbB = new Northwind(connString);
    using (TransactionScope tx = new TransactionScope())
    {
        var order1 = (
            from o in dbA.Orders
            where o.OrderID == 10248
            select o).First();
        order1.ShipPostalCode = DateTime.Now.ToString("HHmmss");
        var order2 = (
            from o in dbB.Orders
            where o.OrderID == 10249
            select o).First();
        order2.ShipPostalCode = DateTime.Now.ToString("HHmmss");
        dbA.SubmitChanges();
        dbB.SubmitChanges();
        tx.Complete();
    }

以上這三種做法,分別就是MSDN文件上提到的Implicit Transaction、Explicit Local Transaction以及Explicit Distributable Transaction。這裡做一下簡單的比較:

  1. Implicit Transaction: 當沒有設定DataContext.Transaction也沒有包TransactionScope時,Call SubmitChanges()會自動將新增/更新/刪除動作包成交易,最為簡便。但要注意,它限於同一個DataConext(當然,同一個DB Connection),且Transaction的範圍無法包含SELECT。
  2. Explicit Local Transaction: 前題是參與Transaction的DataContext要共用一條Connection,且要自己完成BeginTransaction、Commit、Rollback等動作。設定DataContext.Transaction的時機可以決定是否將SELECT也納入Transaction範圍。還有一點限制是,這種做法只限同一台DB,不支援分散式交易。
  3. Explicit Distributable Transaction: 彈性最大,可支援異種資料庫的分散式交易,但要注意所有包含在TransactionScope中的資料庫動作都會啟用LTM或OleTx,成本頗為昂貴(輕巧的LTM適用的條件很嚴苛,只限SQL 2005,如果TransactionScope中有涉及一台以上DB或對遠端SQL 2005開啟兩條以上連線,一定是用貴森森的OleTx),可以視需要將不必參與Transaction的部分隔離開來,以增進效能。(延伸閱讀: .NET分散式交易程式開發FAQ )

在探索Linq的過程中,我開始覺得,"擅長SQL"漸漸不再是程式開發者數一數二的重要技能,Linq To SQL讓開發者可以在完全不懂SQL的情況下寫出資料庫存取程式;表面上開發者的技術門檻變低是好事,但不免開始擔心這個進步背後隱藏的黑暗面

未來,極有可能出現一行SQL都不懂的開發者拿著Linq這把新時代的機關槍,完全不瞄準就抱著四處掃射,任意寫幾行Code就發出一堆無效率的SQL查詢(大方地享用DataContext的自動關聯,一口氣由業務員查客戶,由客戶查訂單,再由訂單查明細)、成串混雜的Transaction(只要一路更動幾十個物件,再一次SubmitChanges()就可以辦到)。屆時,資料庫很可能被一堆無效率的查詢拖累而近癱瘓,一堆非必要的更新被自動包成Transaction則無疑開啟了Deadlock地獄的大門...

我想,一路從ADO、ADO.NET走來的老兵都深知前述的預言並非杞人憂天,所以記得要多說些鬼故事,提醒這群直接由Linq入門的菜鳥開發者一定要花時間了解Linq背後的SQL效能議題。Linq會變成化繁為簡的天使,還是拖垮資料庫的惡魔,關鍵握在他們手上。稍一不慎,Linq很可能就將變成聲名狼藉的資料庫殺手!


Comments

# by wilsont

very useful! thank you!

# by Enosh

你後面的結語我有同感,現在很多人都已經不懂ER Model了

# by Maxi

LINQ真的好用嗎? 現在看到C#語法混雜著linq再加上不負責任者的命名方法 我想以後要trace code真的會死人 更不用說Lambda equation. 而且我最怕就是用VS拉一拉的那種developer 跟他們合作可會搞死人(不懂sql,不懂life cycle,etc)

# by Jeffrey

to Maxi, 你的確點到了重點--LINQ非常依賴IDE環境提供的支援! 在我看到的大部分LINQ程式裡,p, q, o之類的變數生命週期多半只在幾列Code之間而已,因此命名就變得不怎麼重要,反正IDE會自動提示帶出其對應的屬性。 但誠然如你所說,如果把VS2008拿走,限用一般文字編輯器來維護LINQ程式碼,應該會讓人有想自盡的衝動吧!

# by Maxi

所以總而言之,LinQ應該還算不錯,減少很多開發時間 我只是盡量不想依賴IDE 因為如果某天老闆說 我們把source migrate到Linux+Java+Hibernate 那要重寫LinQ的部份會死得很難看

# by Simon

請問所謂「兩個DataContext共用一條Connection」 ConnectionString要怎麼寫? 撰寫Linq to SQL過程中 Visual Studio自動產生出的ConnectionString的長相都是 「Data Source=10.1.2.3;Initial Catalog=DB1;Persist Security Info=True;User ID=user1;Password=user1pw」 也就是裡面都已指定是哪個DB了 不同DataContext指定的DB自然不同 那我有在同一台SQL Server的兩個DB/DataContext要一起交易 要怎麼用同一個ConnectionString呢? 是把「Initial Catalog」那部份刪掉就好嗎? 這樣那兩個DataContext會各自操作到正確的DB嗎? 懇請賜教,謝謝。

# by Jeffrey

to Simon, 補充一下,文中指的是"不同DataContext Instance共用一條連線"的情境。例如: 有A(), B()兩個Method,在其中各自建立了BooDataContext() (同一個DataContext Class,但執行時是兩個物件個體),並用它做了一些資料庫更新,若我希望A(), B()同用一條啟用Transaction模式的連線,由連線來控制A()/B()執行動作的Commit/Rollback,就適用第二點的狀況。若A()/B()涉及的是連向不同DB的DataContext(例如: A()用BooDataContext,B()用FooDataContext),用TransactionScope應是較簡單的做法。

Post a comment