了解Entity Framework中事务处理
? ??? Entity Framework 6以前,框架本身并沒(méi)有提供顯式的事務(wù)處理方案,在EF6中提供了事務(wù)處理的API。
????? 所有版本的EF,只要你調(diào)用SaveChanges方法進(jìn)行插入、修改或刪除,EF框架會(huì)自動(dòng)將該操作進(jìn)行事務(wù)包裝。這種方法無(wú)法對(duì)事務(wù)進(jìn)行顯式的控制,例如新建事務(wù)等,可能會(huì)造成事務(wù)的粒度非常大,降低效率。EF不會(huì)對(duì)查詢進(jìn)行事務(wù)包裝。
???? 從EF6開(kāi)始,默認(rèn)情況下,如果每次調(diào)用Database.ExecuteSqlCommand(),如果其不在存在于任何事務(wù)中,則會(huì)將該Command包裝到一個(gè)事務(wù)中。框架提供了多種重載,允許你重寫(xiě)這些方法,實(shí)現(xiàn)事務(wù)的控制。同樣,執(zhí)行存儲(chǔ)過(guò)程的ObjectContext.ExecuteFunction()方法是實(shí)現(xiàn)了這種機(jī)制(但是ExecuteFunction不能被重寫(xiě))。這兩種情況下,使用的事務(wù)隔離級(jí)別均為數(shù)據(jù)庫(kù)提供的默認(rèn)隔離級(jí)別,SQL Server中使用的是READ COMMITED。
????? 有同學(xué)提供了EF6之前版本的事務(wù)方案,如下:
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 //提交事務(wù) 13 transaction.Complete(); 14 } 15 }????? 其實(shí),上面方法執(zhí)行結(jié)果不會(huì)錯(cuò),但是存在隱患,這樣情況下,顯式事務(wù)其實(shí)是多余的。所以我對(duì)這種方案持懷疑態(tài)度(沒(méi)有進(jìn)行內(nèi)部代碼的分析,有時(shí)間了分析下,希望大家拍磚)。
????? 官方體統(tǒng)的解決方案為:
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 }?
????? 一般情況下,用戶不需要對(duì)事務(wù)進(jìn)行特殊的控制,使用EF框架默認(rèn)行為即可。如果要對(duì)細(xì)節(jié)進(jìn)行控制,參考下面章節(jié):
EF6 API工作機(jī)制
EF6以前版本EF框架自己管理數(shù)據(jù)庫(kù)連接,如果你自己嘗試打開(kāi)連接可能會(huì)拋出異常(打開(kāi)一個(gè)已打開(kāi)的連接會(huì)拋出異常)。由于事務(wù)必須在一個(gè)打開(kāi)的連接上執(zhí)行,因此要合并一系列操作到一個(gè)事務(wù)中,要么使用TractionScope,要么使用ObjectContext.Connection屬性直接執(zhí)行EntityConnection的Open(),并BeginTransaction()。另外,如果你在數(shù)據(jù)庫(kù)底層連接上執(zhí)行了事務(wù),上面API會(huì)失敗。
注意:EF6中移除了僅接受關(guān)閉連接的限制。
EF6 開(kāi)始提供了:
Database.BeginTransaction() : 為用戶提供一種簡(jiǎn)單易用的方案,在DbContext中啟動(dòng)并完成一個(gè)事務(wù) -- 合并一系列操作到該事務(wù)中。同時(shí)使用戶更方便的指定事務(wù)隔離級(jí)別。
Database.UseTransaction() : 允許DbContext使用一個(gè)EF框架外的事務(wù)。
在同一DbContext中合并一系列操作到一個(gè)事務(wù)中
Database.BeginTransaction()有兩個(gè)重載方法。一個(gè)方法提供一個(gè)IsolationLevel參數(shù),另一個(gè)無(wú)參方法使用底層數(shù)據(jù)庫(kù)提供程序默認(rèn)的數(shù)據(jù)庫(kù)事務(wù)隔離級(jí)別。兩個(gè)重載方法均返回一個(gè)DbContextTransaction對(duì)象,該對(duì)象提供Commit和Rollback方法,用于數(shù)據(jù)庫(kù)底層事務(wù)的提交和回滾。
使用DbContextTransaction意味著,一旦提交或回滾事務(wù),就要釋放該對(duì)象。一種簡(jiǎn)單的方法是使用using語(yǔ)法,在using代碼塊結(jié)束時(shí)自動(dòng)調(diào)用該對(duì)象的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 }注意:啟動(dòng)一個(gè)事務(wù)需要底層數(shù)據(jù)庫(kù)連接已打開(kāi)。因此,如果連接未打開(kāi),調(diào)用Database.BeginTransaction()會(huì)打開(kāi)連接,在其Dispose時(shí)關(guān)閉連接。
傳遞一個(gè)現(xiàn)有事務(wù)到DbContext
????? 有時(shí),你可能需要在同一數(shù)據(jù)庫(kù)上,執(zhí)行一個(gè)EF框架之外更大范圍的事務(wù),這是就需要自己打開(kāi)連接并啟動(dòng)事務(wù),然后通知EF框架:
1) 使用已打開(kāi)的數(shù)據(jù)庫(kù)連接
2) 在該連接上使用現(xiàn)有的事務(wù)
????? 要實(shí)現(xiàn)上面的行為,你需要使用繼承自DbContext的構(gòu)造方法XXXContext(conn,contextOwnsConnection),其中:
?????????????????? conn : 是一個(gè)已存在的數(shù)據(jù)庫(kù)連接
???????????????????contextOwnsConnection : 是一個(gè)布爾值,指示上下文是否自己占用數(shù)據(jù)庫(kù)連接。
注意:這種情況下,contextOwnsConnection必須設(shè)置為false,因?yàn)樗ㄖ狤F框架,在自己使用完連接后,不要關(guān)閉它。見(jiàn)下面代碼:
1 using (var conn = new SqlConnection("...")) 2 { 3 conn.Open(); 4 using (var context = new BloggingContext(conn, contextOwnsConnection: false)) 5 { 6 } 7 }????? 此外,你必須自己?jiǎn)?dòng)事務(wù)(如果你不想使用默認(rèn)IsolationLevel,可以自己設(shè)置之)并讓EF框架知道該連接上已經(jīng)存在已啟動(dòng)的事務(wù)(參考下面代碼的33行)。
????? 然后就可以直接在連接上執(zhí)行數(shù)據(jù)庫(kù)操作,或者在DbContext上執(zhí)行,所有這些操作均在同一事務(wù)中執(zhí)行,你負(fù)責(zé)提交或回滾事務(wù),并調(diào)用DatabaseTransaction.Dispose(),最后要關(guān)閉和釋放數(shù)據(jù)庫(kù)連接。請(qǐng)參考以下代碼:
注意:
- 你可以傳遞null到方法Database.UseTransaction()來(lái)清除EF框架對(duì)當(dāng)前事務(wù)的記憶。如果你這樣做,事務(wù)既不會(huì)提交也不會(huì)回滾。所以要謹(jǐn)慎使用之,除非你確實(shí)需要這樣。
- 如果EF框架已經(jīng)持有一個(gè)事務(wù),此時(shí)你傳遞一個(gè)事務(wù),Database.UseTransaction()將拋出一個(gè)異常:
?????? ★ EF框架已經(jīng)持有一個(gè)事務(wù);
?????? ★ 當(dāng)EF框架已經(jīng)在一個(gè)TransactionScope中運(yùn)行;
?????? ★ 其數(shù)據(jù)庫(kù)連接對(duì)象為null (例如,無(wú)連接--通常這種情況表示事務(wù)已經(jīng)完成);
?????? ★ 數(shù)據(jù)庫(kù)連接對(duì)象與EF框架的數(shù)據(jù)庫(kù)連接對(duì)象不匹配;
?對(duì)TransactionScope的一些補(bǔ)充
如果你使用.net framework 4.5.1及以上版本,可以使用TransactionScope的TransactionScopeAsyncFlowOption參數(shù)提供對(duì)異步的支持:
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及以上版本才支持異步方法;
- 不能適用于云方案(除非你確保只有一個(gè)連接 -- 云方案不支持分布式事務(wù));
- 不能和Database.UseTransaction()結(jié)合使用;
- 如果你的DDL代碼存在問(wèn)題(例如數(shù)據(jù)庫(kù)初始化問(wèn)題)或沒(méi)有通過(guò)MSDTC服務(wù)來(lái)支持分布式事務(wù),將拋出異常;
使用TransactionScope的優(yōu)點(diǎn):
- 自動(dòng)將本地事務(wù)升級(jí)為分布式事務(wù):前提是你有不止一個(gè)連接到給定數(shù)據(jù)庫(kù)或要組合一個(gè)連接到另一個(gè)數(shù)據(jù)庫(kù)連接到同一事務(wù)(注意:你必須啟動(dòng)MSDTC服務(wù)以支持分布式事務(wù))。
- 易于編程。如果你更希望淡化對(duì)事務(wù)的關(guān)注,而非顯示操作事務(wù),使用TransactionScope將是一個(gè)更合適的選擇。
?
????? 隨著EF6提供了Database.BeginTransaction()和Database.UseTransaction() 兩個(gè)API,使用TransactionScope不在是必須的了。如果你依然使用TransactionScope,就必須留意上面限制。建議你盡可能使用新的API,而非TransactionScope。
總結(jié)
以上是生活随笔為你收集整理的了解Entity Framework中事务处理的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 信用卡注销后还可以再申请吗
- 下一篇: ros中的坐标系,