處理交易

備註

僅限 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 之前,實體框架堅持要自己開啟資料庫連線(如果被傳入已開啟的連線,會拋出例外)。 由於交易只能在開啟連線上啟動,這意味著使用者若要將多個操作包裝成一筆交易,唯一方法是使用 TransactionScopeObjectContext.Connection 屬性,直接呼叫回傳的 EntityConnection 物件 Open()BeginTransaction()。 此外,若你自己在底層資料庫連線上啟動交易,呼叫到資料庫的 API 呼叫也會失敗。

備註

在 Entity Framework 6 中,僅接受封閉連接的限制被移除。 詳情請參見 連線管理

從 EF6 開始,框架現提供:

  1. Database.BeginTransaction():一種讓使用者在現有的 DbContext 中自行開始並完成事務的簡單方法,允許在同一事務中合併多個操作,從而全部提交或全部回滾為一。 同時也讓使用者更容易指定交易的隔離層級。
  2. 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,請注意上述限制。 我們建議盡可能採用前述章節所述的做法。