被問到在EF環境要如何控制將某些DB操作包含在Transaction範圍內、將某些排除在外? 整理成簡單範例方便說明。

範例程式碼共有三段DB操作,第一段是寫入追蹤資訊到ActLog資料表、第二、三段則是各寫入一筆Player資料,為了模擬交易Rollback情境,故意讓兩筆Player的Primary Key相同。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Transactions;
 
namespace TestNoTran
{
    class Program
    {
        static void Main(string[] args)
        {
            using (TransactionScope tx = new TransactionScope())
            {
                try
                {   //...其他程式邏輯(省略)...
                    SomeLogHelper.Log("Debug: Insert To DB");
                    SomeDALHelper.InsertPlayer("A1", "U1", DateTime.Today, 100);
                    //故意寫入PK相同的第二筆資料,將引發錯誤
                       SomeDALHelper.InsertPlayer("A1", "U2", DateTime.Today, 120);
                    tx.Complete();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error: {0}", ex.Message);
                }
                Console.Read();
            }
 
        }
    }
 
    class SomeDALHelper
    {
        public static void InsertPlayer(
            string id, string name, DateTime regDate, int score)
        {
            using (var ctx = new LabEntities())
            {
                var p1 = new Player()
                {
                    UserId = id,
                    UserName = name,
                    RegDate = regDate,
                    Score = score
                };
                ctx.Players.Add(p1);
                ctx.SaveChanges();
            }
        }
    }
 
    class SomeLogHelper 
    {
        public static void Log(string msg)
        {
            using (var ctx = new LabEntities())
            {
                ctx.ActLogs.Add(new ActLog()
                {
                    Info = msg
                });
                ctx.SaveChanges();
            }
        }
    }
}

執行程式,一如預期,會得到因PK重複引發的錯誤:

Error: System.Data.Entity.Infrastructure.DbUpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.UpdateException: An error occurred while updating the entries. See the inner exception for details. ---> System.Data.SqlClient.SqlException: Violation of PRIMARY KEY constraint 'PK_Player'. Cannot insert duplicate key in object 'dbo.Player'. The duplicate key value is (A1).

檢查資料庫,ActLog及Player資料表均空空如也,代表三個DB動作一起Rollback了。如果我們希望寫ActLog的部分不要參與交易,無論如何都將資訊寫入資料庫,該怎麼做?

最簡單的做法是用另一個TransactionScope將寫ActLog部分包起來,並指定初始化參數為TransactionScopeOption.Suppress,宣告在此範圍內的DB動作不需要參與交易。如下:

        static void Main(string[] args)
        {
            using (TransactionScope tx = new TransactionScope())
            {
                try
                {   //...其他程式邏輯(省略)...
                    //宣告出一段不參與交易的範圍
                        using (TransactionScope tx2 = 
                        new TransactionScope(TransactionScopeOption.Suppress))
                    {
                        SomeLogHelper.Log("Debug: Insert To DB");
                    }
                    SomeDALHelper.InsertPlayer("A1", "U1", DateTime.Today, 100);
                    //故意寫入PK相同的第二筆資料,將引發錯誤
    SomeDALHelper.InsertPlayer("A1", "U2", DateTime.Today, 120);
                    tx.Complete();
                }
                catch (Exception ex)
                {
                    Console.WriteLine("Error: {0}", ex.ToString());
                }
                Console.Read();
            }
 
        }

重新執行程式,錯誤依舊,Player資料表仍無資料,但ActLog資料表會留下一筆記錄,實現了排除在交易範圍外的目標。

最後補充,除了使用TransactionScope外,還有其他處理EF交易的方式,如: 讓DbContext.SaveChanges()自動將一連串動作包成交易、讓多個DbContext共用連線並控制該連線交易狀態等,細節可參考舊文,該文談的雖是LINQ to SQL,但與EF運作原理大同小異。


Comments

# by Eric

敢問黑大: EF撘配TransactionScope 可以做到 rowlock嗎? 我有一些交易很頻繁的資料,想要避免更新時被查詢. 目前是另外寫成 stored procedure 但總是麻煩了點. 不知黑大有沒有解法.

# by Jeffrey

to Eric, 太客氣了,既然您用了"敢問",我只好斗膽回答(哈!) 依我的理解,交易時要使用Row Lock、Page Lock還是Table Lock,多半由資料庫系統核心自主管控,屬於非常底層的實作細節,而EF或TransactionScope偏向高階的資料操作邏輯,並不會與特定資料庫系統綁在一起(尤其各家DBMS實現交易的機制也有所不同,例如: SQL傳統上偏向用Lock,Oracle則傾向MVCC。註: 新版SQL也支援MVCC了,關鍵字ALLOW_SNAPSHOT_ISOLATION),因此我認為無法由EF或TransactionScope的角度去控制Lock階級,直接寫SQL指令看來是唯一的管道。

# by Eric

呵~~謝謝黑大的回答 黑大在本篇提到的"舊文"中 BeginTransaction 有沒有包含SELECT的差別是? 會是row lock嗎?

# by Jeffrey

to Eric, 放Shared Lock還是Exclusive Lock? Lock範圍應該限於KEY(RID, Row ID Lock)、Page還是乾脆鎖Table? 這兩個是彼此獨立的議題,為了確保資料真確性,在交易中出現的SELECT或者需要動用Lock機制,防止前後二次讀取結果不同。但每次要鎖定的範圍是KEY、Page還是Table,則由DBMS依查詢條來決定,原則上它會考量效能、共用性。Begin Tran中的SELECT會觸發一些Lock,但至於範圍多大,則要視查詢條件或你所提供的Hint來決定。推薦一篇好文章: http://www.sql-server-performance.com/2004/advanced-sql-locking/

# by 鮪魚

資料庫一般會確保ACID的特性。所以當寫入一筆資料時,基本上這筆資料還沒完全寫入之前是沒辦法被讀取的。可以想像transaction具有atomic的特性,它的寫入,是一個無法再被切割的operation,不會有寫到一半然後有人來干擾。 除非修改了Transaction的設定,允許可以讀取,還沒有被Commited的資料。但一般人是不會去做這種事情的。

Post a comment