備註
僅限 EF6 及以上 版本 - 本頁討論的功能、API 等皆於 Entity Framework 6 中引入。 如果你使用的是較早期版本,部分或全部資訊可能不適用。
本文件將說明如何在 EF6 中使用交易,包括自 EF5 以來新增的改進,使交易操作更簡便。
EF 預設的功能
在所有版本的 Entity Framework 中,每當你執行 SaveChanges() 來插入、更新或刪除資料庫時,框架會將該操作包裝成交易。 此交易僅持續足夠執行操作後完成。 當你執行另一個此類操作時,會啟動一個新的交易。
從 EF6 開始,Database.ExecuteSqlCommand() 預設會將該指令包裹在交易中,如果尚未存在交易。 這種方法有過載功能,讓你可以選擇覆蓋這個行為。 此外,在 EF6 中,透過 ObjectContext.ExecuteFunction() 等 API 執行模型中包含的儲存程序也有相同效果(但預設行為目前無法覆寫)。
無論哪種情況,交易的隔離層級都是資料庫提供者認為其預設設定的隔離層級。 例如,在 SQL Server 上,預設情況下為 READ COMMITTED。
Entity Framework 不會將查詢包裝成交易。
此預設功能適合許多使用者,若適用,則在 EF6 中無需做任何改變;照你平常的方式寫程式碼就好。
然而,有些使用者需要對交易有更大的控制權——這部分將在以下章節中說明。
API 的運作方式
在 EF6 之前,實體框架堅持要自己開啟資料庫連線(如果被傳入已開啟的連線,會拋出例外)。 由於交易只能在開啟連線上啟動,這意味著使用者若要將多個操作包裝成一筆交易,唯一方法是使用 TransactionScope 或 ObjectContext.Connection 屬性,直接呼叫回傳的 EntityConnection 物件 Open() 和 BeginTransaction()。 此外,若你自己在底層資料庫連線上啟動交易,呼叫到資料庫的 API 呼叫也會失敗。
備註
在 Entity Framework 6 中,僅接受封閉連接的限制被移除。 詳情請參見 連線管理。
從 EF6 開始,框架現提供:
- Database.BeginTransaction():一種讓使用者在現有的 DbContext 中自行開始並完成事務的簡單方法,允許在同一事務中合併多個操作,從而全部提交或全部回滾為一。 同時也讓使用者更容易指定交易的隔離層級。
- Database.UseTransaction() : 允許 DbContext 使用在實體框架外啟動的交易。
在同一情境下將多個操作合併成一個交易
Database.BeginTransaction() 有兩種重載方法——一種接受明確指定的 IsolationLevel,另一種不接受參數,並使用底層資料庫提供者的預設 IsolationLevel。 兩種覆寫都會回傳 DbContextTransaction 物件,該物件提供 Commit() 與 Rollback() 方法,對底層的儲存交易執行提交與回滾。
DbContextTransaction 應在提交或回滾後被處置。 一個簡單的方法是使用 (...) {...} 語法,當使用區塊完成時,會自動呼叫 Dispose() :
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void StartOwnTransactionWithinContext()
{
using (var context = new BloggingContext())
{
using (var dbContextTransaction = context.Database.BeginTransaction())
{
context.Database.ExecuteSqlCommand(
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'"
);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
dbContextTransaction.Commit();
}
}
}
}
}
備註
開始交易需要底層的儲存連接是開放的。 所以如果連線還沒開啟,呼叫 Database.BeginTransaction() 將會開啟連線。 如果 DbContextTransaction 開啟了連線,那麼當呼叫 Dispose() 時,它會關閉該連線。
將現有交易傳遞到上下文中
有時你希望交易範圍更廣,包含在同一資料庫上的操作,但完全不在 EF 之外。 要做到這點,你必須自己開啟連線並啟動交易,然後告訴 EF a) 使用已開啟的資料庫連線,b) 使用該連線上的現有交易。
為此,你必須在上下文類別中定義並使用一個建構子,該建構子繼承自其中一個 DbContext 建構子,該建構子取 i) 一個現有的連接參數,ii) contextOwnsConnection 布林值。
備註
在此情境中,當被呼叫時,contextOwnsConnection 標誌必須設為 false。 這很重要,因為它告知實體框架在完成連線後不應關閉(例如,請見下方第 4 行):
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
}
}
此外,你必須自己啟動交易(包括 IsolationLevel,以避免預設設定),並告知 Entity Framework 連線上已有交易開始(見下方第 33 行)。
接著你可以直接在 SqlConnection 本身或 DbContext 上執行資料庫操作。 所有此類操作皆在一筆交易中執行。 你負責提交或回復該交易並呼叫其上的 Dispose(),以及關閉和釋放資料庫連線。 例如:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void UsingExternalTransaction()
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
{
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.Transaction = sqlTxn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
context.Database.UseTransaction(sqlTxn);
var query = context.Posts.Where(p => p.Blog.Rating >= 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
sqlTxn.Commit();
}
}
}
}
}
釐清交易
你可以將 null 傳給 Database.UseTransaction(),以清除 Entity Framework 對目前交易的知識。 Entity Framework 在你這麼做時既不會提交也不會回滾現有交易,所以請謹慎使用,只有在你確定這是你想做的事時才會使用。
UseTransaction 中的錯誤
當你在以下情況下通過交易,會看到 Database.UseTransaction() 的例外:
- Entity Framework 已經有現有的交易
- Entity Framework 已經在 TransactionScope 中運作
- 傳遞的交易中的連線物件為空。 也就是說,該交易並未與連線相關聯——通常表示該交易已經完成
- 交易中傳遞的連接物件與實體框架的連接不符。
使用其他功能的交易
本節詳述上述交易如何與以下交互作用:
- 連線復原能力
- 非同步方法
- TransactionScope 交易
連線穩定性
新的連線韌性功能無法支援使用者發起的交易。 詳情請參見 「重試執行策略」。
異步程序設計
前述方法不需要其他選項或設定即可使用 非同步查詢與儲存方法。 但要注意,根據你在非同步方法中的操作,這可能導致交易長時間運行,進而造成死結或阻塞,對整體應用程式的效能很不利。
TransactionScope 交易
在 EF6 之前,提供更大範圍交易的建議方式是使用 TransactionScope 物件:
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
static void UsingTransactionScope()
{
using (var scope = new TransactionScope(TransactionScopeOption.Required))
{
using (var conn = new SqlConnection("..."))
{
conn.Open();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
sqlCommand.ExecuteNonQuery();
using (var context =
new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
context.SaveChanges();
}
}
scope.Complete();
}
}
}
}
SqlConnection 與 Entity Framework 都會使用 Ambient TransactionScope,因此會同時提交。
從 .NET 4.5.1 開始,TransactionScope 也更新為可透過 TransactionScopeAsyncFlowOption 枚舉支援非同步方法:
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Transactions;
namespace TransactionsExamples
{
class TransactionsExample
{
public static void AsyncTransactionScope()
{
using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
{
using (var conn = new SqlConnection("..."))
{
await conn.OpenAsync();
var sqlCommand = new SqlCommand();
sqlCommand.Connection = conn;
sqlCommand.CommandText =
@"UPDATE Blogs SET Rating = 5" +
" WHERE Name LIKE '%Entity Framework%'";
await sqlCommand.ExecuteNonQueryAsync();
using (var context = new BloggingContext(conn, contextOwnsConnection: false))
{
var query = context.Posts.Where(p => p.Blog.Rating > 5);
foreach (var post in query)
{
post.Title += "[Cool Blog]";
}
await context.SaveChangesAsync();
}
}
scope.Complete();
}
}
}
}
TransactionScope 方法仍有一些限制:
- 需要 .NET 4.5.1 或以上版本才能支援非同步方法。
- 除非你確定只有一個連線,否則無法在雲端情境中使用(雲端情境不支援分散式交易)。
- 它無法與前述章節的 Database.UseTransaction() 方法結合使用。
- 如果你發出任何 DDL 且未啟用 MSDTC 服務的分散式交易,系統會拋出例外。
TransactionScope 方法的優點:
- 如果你在同一個資料庫建立多於一個連線,或在同一交易內將一個資料庫的連線與另一個資料庫的連線結合,它會自動將本地交易升級為分散式交易(注意:你必須設定 MSDTC 服務才能允許分散式交易,才能運作)。
- 程式設計的簡單性。 如果你偏好交易是環境式的,且在背景中隱含處理,而非明確由你控制,那麼 TransactionScope 的方法可能更適合你。
總結來說,隨著上述新增的 Database.BeginTransaction() 與 Database.UseTransaction() API,TransactionScope 方法對大多數使用者已不再必要。 如果你繼續使用 TransactionScope,請注意上述限制。 我們建議盡可能採用前述章節所述的做法。