控制EF的Transaction範圍
5 |
被問到在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的資料。但一般人是不會去做這種事情的。