Entity Framework 6以前,框架本身並沒有提供顯式的事務處理方案,在EF6中提供了事務處理的API。
所有版本的EF,只要你調用SaveChanges方法進行插入、修改或刪除,EF框架會自動將該操作進行事務包裝。這種方法無法對事務進行顯式的控制,例如新建事務等,可能會造成事務的粒度非常大,降低效率。EF不會對查詢進行事務包裝。
從EF6開始,默認情況下,如果每次調用Database.ExecuteSqlCommand(),如果其不在存在於任何事務中,則會將該Command包裝到一個事務中。框架提供了多種重載,允許你重寫這些方法,實現事務的控制。同樣,執行存儲過程的ObjectContext.ExecuteFunction()方法是實現了這種機制(但是ExecuteFunction不能被重寫)。這兩種情況下,使用的事務隔離級別均為數據庫提供的默認隔離級別,SQL Server中使用的是READ COMMITED。
有同學提供了EF6之前版本的事務方案,如下:
1 using (BlogDbContext context =new BlogDbContext()) 2 { 3 using (TransactionScope transaction =new TransactionScope()) 4 { 5 context.BlogPosts.Add(blogPost); 6 context.SaveChanges(); 7 postBody.ID = blogPost.ID; 8 context.EntryViewCounts.Add( 9 new EntryViewCount() { EntryID = blogPost.ID }); 10 context.PostBodys.Add(postBody); 11 context.SaveChanges(); 12 //提交事務 13 transaction.Complete(); 14 } 15 }
其實,上面方法執行結果不會錯,但是存在隱患,這樣情況下,顯式事務其實是多余的。所以我對這種方案持懷疑態度(沒有進行內部代碼的分析,有時間了分析下,希望大家拍磚)。
官方體統的解決方案為:
1 using System.Collections.Generic; 2 using System.Data.Entity; 3 using System.Data.SqlClient; 4 using System.Linq; 5 using System.Transactions; 6 7 namespace TransactionsExamples 8 { 9 class TransactionsExample 10 { 11 static void UsingTransactionScope() 12 { 13 using (var scope = new TransactionScope(TransactionScopeOption.Required)) 14 { 15 using (var conn = new SqlConnection("...")) 16 { 17 conn.Open(); 18 19 var sqlCommand = new SqlCommand(); 20 sqlCommand.Connection = conn; 21 sqlCommand.CommandText = 22 @"UPDATE Blogs SET Rating = 5" + 23 " WHERE Name LIKE '%Entity Framework%'"; 24 sqlCommand.ExecuteNonQuery(); 25 26 using (var context = 27 new BloggingContext(conn, contextOwnsConnection: false)) 28 { 29 var query = context.Posts.Where(p => p.Blog.Rating > 5); 30 foreach (var post in query) 31 { 32 post.Title += "[Cool Blog]"; 33 } 34 context.SaveChanges(); 35 } 36 } 37 38 scope.Complete(); 39 } 40 } 41 } 42 }
一般情況下,用戶不需要對事務進行特殊的控制,使用EF框架默認行為即可。如果要對細節進行控制,參考下面章節:
EF6 API工作機制
EF6以前版本EF框架自己管理數據庫連接,如果你自己嘗試打開連接可能會拋出異常(打開一個已打開的連接會拋出異常)。由於事務必須在一個打開的連接上執行,因此要合並一系列操作到一個事務中,要么使用TractionScope,要么使用ObjectContext.Connection屬性直接執行EntityConnection的Open(),並BeginTransaction()。另外,如果你在數據庫底層連接上執行了事務,上面API會失敗。
注意:EF6中移除了僅接受關閉連接的限制。
EF6 開始提供了:
Database.BeginTransaction() : 為用戶提供一種簡單易用的方案,在DbContext中啟動並完成一個事務 -- 合並一系列操作到該事務中。同時使用戶更方便的指定事務隔離級別。
Database.UseTransaction() : 允許DbContext使用一個EF框架外的事務。
在同一DbContext中合並一系列操作到一個事務中
Database.BeginTransaction()有兩個重載方法。一個方法提供一個IsolationLevel參數,另一個無參方法使用底層數據庫提供程序默認的數據庫事務隔離級別。兩個重載方法均返回一個DbContextTransaction對象,該對象提供Commit和Rollback方法,用於數據庫底層事務的提交和回滾。
使用DbContextTransaction意味着,一旦提交或回滾事務,就要釋放該對象。一種簡單的方法是使用using語法,在using代碼塊結束時自動調用該對象的Dispose方法。
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Data.SqlClient; 5 using System.Linq; 6 using System.Transactions; 7 8 namespace TransactionsExamples 9 { 10 class TransactionsExample 11 { 12 static void StartOwnTransactionWithinContext() 13 { 14 using (var context = new BloggingContext()) 15 { 16 using (var dbContextTransaction = context.Database.BeginTransaction()) 17 { 18 try 19 { 20 context.Database.ExecuteSqlCommand( 21 @"UPDATE Blogs SET Rating = 5" + 22 " WHERE Name LIKE '%Entity Framework%'" 23 ); 24 25 var query = context.Posts.Where(p => p.Blog.Rating >= 5); 26 foreach (var post in query) 27 { 28 post.Title += "[Cool Blog]"; 29 } 30 31 context.SaveChanges(); 32 33 dbContextTransaction.Commit(); 34 } 35 catch (Exception) 36 { 37 dbContextTransaction.Rollback(); 38 } 39 } 40 } 41 } 42 } 43 }
注意:啟動一個事務需要底層數據庫連接已打開。因此,如果連接未打開,調用Database.BeginTransaction()會打開連接,在其Dispose時關閉連接。
傳遞一個現有事務到DbContext
有時,你可能需要在同一數據庫上,執行一個EF框架之外更大范圍的事務,這是就需要自己打開連接並啟動事務,然后通知EF框架:
1) 使用已打開的數據庫連接
2) 在該連接上使用現有的事務
要實現上面的行為,你需要使用繼承自DbContext的構造方法XXXContext(conn,contextOwnsConnection),其中:
conn : 是一個已存在的數據庫連接
contextOwnsConnection : 是一個布爾值,指示上下文是否自己占用數據庫連接。
注意:這種情況下,contextOwnsConnection必須設置為false,因為它通知EF框架,在自己使用完連接后,不要關閉它。見下面代碼:
1 using (var conn = new SqlConnection("...")) 2 { 3 conn.Open(); 4 using (var context = new BloggingContext(conn, contextOwnsConnection: false)) 5 { 6 } 7 }
此外,你必須自己啟動事務(如果你不想使用默認IsolationLevel,可以自己設置之)並讓EF框架知道該連接上已經存在已啟動的事務(參考下面代碼的33行)。
然后就可以直接在連接上執行數據庫操作,或者在DbContext上執行,所有這些操作均在同一事務中執行,你負責提交或回滾事務,並調用DatabaseTransaction.Dispose(),最后要關閉和釋放數據庫連接。請參考以下代碼:
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Data.SqlClient; 5 using System.Linq; 6 sing System.Transactions; 7 8 namespace TransactionsExamples 9 { 10 class TransactionsExample 11 { 12 static void UsingExternalTransaction() 13 { 14 using (var conn = new SqlConnection("...")) 15 { 16 conn.Open(); 17 18 using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot)) 19 { 20 try 21 { 22 var sqlCommand = new SqlCommand(); 23 sqlCommand.Connection = conn; 24 sqlCommand.Transaction = sqlTxn; 25 sqlCommand.CommandText = 26 @"UPDATE Blogs SET Rating = 5" + 27 " WHERE Name LIKE '%Entity Framework%'"; 28 sqlCommand.ExecuteNonQuery(); 29 30 using (var context = 31 new BloggingContext(conn, contextOwnsConnection: false)) 32 { 33 context.Database.UseTransaction(sqlTxn); 34 35 var query = context.Posts.Where(p => p.Blog.Rating >= 5); 36 foreach (var post in query) 37 { 38 post.Title += "[Cool Blog]"; 39 } 40 context.SaveChanges(); 41 } 42 43 sqlTxn.Commit(); 44 } 45 catch (Exception) 46 { 47 sqlTxn.Rollback(); 48 } 49 } 50 } 51 } 52 } 53 }
注意:
- 你可以傳遞null到方法Database.UseTransaction()來清除EF框架對當前事務的記憶。如果你這樣做,事務既不會提交也不會回滾。所以要謹慎使用之,除非你確實需要這樣。
- 如果EF框架已經持有一個事務,此時你傳遞一個事務,Database.UseTransaction()將拋出一個異常:
★ EF框架已經持有一個事務;
★ 當EF框架已經在一個TransactionScope中運行;
★ 其數據庫連接對象為null (例如,無連接--通常這種情況表示事務已經完成);
★ 數據庫連接對象與EF框架的數據庫連接對象不匹配;
對TransactionScope的一些補充
如果你使用.net framework 4.5.1及以上版本,可以使用TransactionScope的TransactionScopeAsyncFlowOption參數提供對異步的支持:
1 using System.Collections.Generic; 2 using System.Data.Entity; 3 using System.Data.SqlClient; 4 using System.Linq; 5 using System.Transactions; 6 7 namespace TransactionsExamples 8 { 9 class TransactionsExample 10 { 11 public static void AsyncTransactionScope() 12 { 13 using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) 14 { 15 using (var conn = new SqlConnection("...")) 16 { 17 await conn.OpenAsync(); 18 19 var sqlCommand = new SqlCommand(); 20 sqlCommand.Connection = conn; 21 sqlCommand.CommandText = 22 @"UPDATE Blogs SET Rating = 5" + 23 " WHERE Name LIKE '%Entity Framework%'"; 24 await sqlCommand.ExecuteNonQueryAsync(); 25 26 using (var context = new BloggingContext(conn, contextOwnsConnection: false)) 27 { 28 var query = context.Posts.Where(p => p.Blog.Rating > 5); 29 foreach (var post in query) 30 { 31 post.Title += "[Cool Blog]"; 32 } 33 34 await context.SaveChangesAsync(); 35 } 36 } 37 } 38 } 39 } 40 }
目前,使用TransactionScope還有一些限制:
- 需要.NET 4.5.1及以上版本才支持異步方法;
- 不能適用於雲方案(除非你確保只有一個連接 -- 雲方案不支持分布式事務);
- 不能和Database.UseTransaction()結合使用;
- 如果你的DDL代碼存在問題(例如數據庫初始化問題)或沒有通過MSDTC服務來支持分布式事務,將拋出異常;
使用TransactionScope的優點:
- 自動將本地事務升級為分布式事務:前提是你有不止一個連接到給定數據庫或要組合一個連接到另一個數據庫連接到同一事務(注意:你必須啟動MSDTC服務以支持分布式事務)。
- 易於編程。如果你更希望淡化對事務的關注,而非顯示操作事務,使用TransactionScope將是一個更合適的選擇。
隨着EF6提供了Database.BeginTransaction()和Database.UseTransaction() 兩個API,使用TransactionScope不在是必須的了。如果你依然使用TransactionScope,就必須留意上面限制。建議你盡可能使用新的API,而非TransactionScope。