首頁 > 軟體

Entity Framework使用Code First模式管理事務

2022-03-05 19:00:42

一、什麼是事務

處理以資料為中心的應用時,另一個重要的話題是事務管理。ADO.NET為事務管理提供了一個非常乾淨和有效的API。因為EF執行在ADO.NET之上,所以EF可以使用ADO.NET的事務管理功能。

當從資料庫角度談論事務時,它意味著一系列操作被當作一個不可分割的操作。所有的操作要麼全部成功,要麼全部失敗。事務的概念是一個可靠的工作單元,事務中的所有資料庫操作應該被看作是一個工作單元。

從應用程式的角度來看,如果我們有多個資料庫操作被當作一個工作單元,那麼應該將這些操作包裹在一個事務中。為了能夠使用事務,應用程式需要執行下面的步驟:

1、開始事務。

2、執行所有的查詢,執行所有的資料庫操作,這些操作被視為一個工作單元。

3、如果所有的事務成功了,那麼提交事務。

4、如果任何一個操作失敗,就回滾事務。

二、建立測試環境

1、提到事務,最經典的例子莫過於銀行轉賬了。我們這裡也使用這個例子來理解一下和事務相關的概念。為了簡單模擬銀行轉賬的情景,假設銀行為不同的賬戶使用了不同的表,對應地,我們建立了OutputAccount和InputAccount兩個實體類,實體類定義如下:

OutputAccount實體類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp.Model
{
    [Table("OutputAccounts")]
    public class OutputAccount
    {
        public int Id { get; set; }
        [StringLength(8)]
        public string Name { get; set; }
        public decimal Balance { get; set; }

    }
}

InputAccount實體類:

using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp.Model
{
    [Table("InputAccounts")]
    public class InputAccount
    {
        public int Id { get; set; }
        [StringLength(8)]
        public string Name { get; set; }
        public decimal Balance { get; set; }

    }
}

2、定義資料上下文類

using EFTransactionApp.Model;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp.EF
{
    public class EFDbContext:DbContext
    {
        public EFDbContext()
            : base("name=AppConnection")
        {

        }


        public DbSet<OutputAccount> OutputAccounts { get; set; }

        public DbSet<InputAccount> InputAccounts { get; set; }
    }
}

3、使用資料遷移生成資料庫,並填充種子資料

namespace EFTransactionApp.Migrations
{
    using EFTransactionApp.Model;
    using System;
    using System.Data.Entity;
    using System.Data.Entity.Migrations;
    using System.Linq;

    internal sealed class Configuration : DbMigrationsConfiguration<EFTransactionApp.EF.EFDbContext>
    {
        public Configuration()
        {
            AutomaticMigrationsEnabled = false;
        }

        protected override void Seed(EFTransactionApp.EF.EFDbContext context)
        {
            //  This method will be called after migrating to the latest version.

            //  You can use the DbSet<T>.AddOrUpdate() helper extension method
            //  to avoid creating duplicate seed data.

            // 填充種子資料
            context.InputAccounts.AddOrUpdate(
                  new InputAccount()
                  {
                      Name = "李四",
                      Balance = 0M
                  }
                );

            context.OutputAccounts.AddOrUpdate(
                 new OutputAccount()
                 {
                     Name="張三",
                     Balance=10000M
                 }
                );
        }
    }
}

4、執行程式

從應用程式的角度看,無論何時使用者將錢從OutputAccount轉入InputAccount,這個操作應該被視為一個工作單元,永遠不應該發生OutputAccount的金額扣除了,而InputAccount的金額沒有增加。接下來我們就看一下EF如何管理事務。

執行程式前,先檢視資料庫資料:

現在,我們嘗試使用EF的事務從OutputAccount的張三轉入1000給InputAccount的李四。

使用EF預設的事務執行

EF的預設行為是:無論何時執行任何涉及Create,Update或Delete的查詢,都會預設建立事務。當DbContext類上的SaveChanges()方法被呼叫時,事務就會提交。

using EFTransactionApp.EF;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new EFDbContext())
            {
                int outputId = 1, inputId = 1;
                decimal transferAmount = 1000m;
                //1 檢索事務中涉及的賬戶
                var outputAccount = db.OutputAccounts.Find(outputId);
                var inputAccount = db.InputAccounts.Find(inputId);
                //2 從輸出賬戶上扣除1000
                outputAccount.Balance -= transferAmount;
                //3 從輸入賬戶上增加1000
                inputAccount.Balance += transferAmount;
                //4 提交事務
                db.SaveChanges();
            }

        }
    }
}

執行程式後,會發現資料庫中資料發生了改變:

可以看到,使用者李四的賬戶上面多了1000,使用者張三的賬戶上面少了1000。因此,這兩個操作有效地被包裹在了一個事務當中,並作為一個工作單元執行。如果任何一個操作失敗,資料就不會發生變化。

可能有人會疑惑:上面的程式執行成功了,沒有看到事務的效果,能不能修改一下程式碼讓上面的程式執行失敗然後可以看到事務的效果呢?答案是肯定可以的,下面將上面的程式碼進行修改。

通過檢視資料庫表結構會發現Balance的資料型別是

意味著Balance列的最大可輸入長度是16位元(最大長度18位元減去2位小數點),如果輸入的長度大於16位元的話程式就會報錯,所以將上面的程式碼進行如下的修改:

using EFTransactionApp.EF;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp
{
    class Program
    {
        static void Main(string[] args)
        {
            using (var db = new EFDbContext())
            {
                int outputId = 1, inputId = 1;
                decimal transferAmount = 1000m;
                //1 檢索事務中涉及的賬戶
                var outputAccount = db.OutputAccounts.Find(outputId);
                var inputAccount = db.InputAccounts.Find(inputId);
                //2 從輸出賬戶上扣除1000
                outputAccount.Balance -= transferAmount;
                //3 從輸入賬戶上增加1000 *3000000000000000倍
                inputAccount.Balance += transferAmount*3000000000000000;
                //4 提交事務
                db.SaveChanges();
            }

        }
    }
}

在次執行程式,會發現程式報錯了:

這時在檢視資料庫,發現使用者張三的餘額還是9000沒有發生變化,說明事務起作用了。

5、使用TransactionScope處理事務

如果有一個場景具有多個DbContext物件,那麼我們想將涉及多個DbContext物件的操作關聯為一個工作單元,這時,我們需要在TransactionScope物件內部包裹SaveChanges()方法的呼叫。為了描述這個場景,我們使用DbContext類的兩個不同範例來執行扣款和收款,程式碼如下:

int outputId = 1, inputId = 1;
decimal transferAmount = 1000m;
using (var ts = new TransactionScope(TransactionScopeOption.Required))
{
       var db1 = new EFDbContext();
       var db2 = new EFDbContext();
       //1 檢索事務中涉及的賬戶
       var outputAccount = db1.OutputAccounts.Find(outputId);
       var inputAccount = db2.InputAccounts.Find(inputId);
       //2 從輸出賬戶上扣除1000
       outputAccount.Balance -= transferAmount;
       //3 從輸入賬戶上增加1000
       inputAccount.Balance += transferAmount;
       db1.SaveChanges();
       db2.SaveChanges();
       ts.Complete();
}

在上面的程式碼中,我們使用了兩個不同的DbContext範例來執行扣款和收款操作。因此,預設的EF行為不會工作。在呼叫各自的SaveChanges()方法時,和上下文相關的各個事務不會提交。相反,因為它們都在 TransactionScope物件的內部,所以,當TransactionScope物件的Complete()方法呼叫時,事務才會提交。如果任何一個操作失敗,就會發生異常,TransactionScope就不會呼叫Complete()方法,從而回滾更改。事務執行失敗的案例也可以按照上面的方式進行修改,使Balance列的長度超過最大長度,這裡就不在演示了。

三、使用EF6管理事務

從EF6開始,EF在DbContext物件上提供了Database.BeginTransaction()方法,當使用上下文類在事務中執行原生SQL命令時,這個方法特別有用。

接下來看一下如何使用這個新方法管理事務。這裡我們使用原生SQL從OutputAccount賬戶中扣款,使用模型類給InputAccount收款,程式碼如下:

int outputId = 1, inputId = 1; decimal transferAmount = 1000m;
using (var db = new EFDbContext())
{
                using (var trans = db.Database.BeginTransaction())
                {
                    try
                    {
                        var sql = "Update OutputAccounts set Balance=Balance-@amountToDebit where id=@outputId";
                        db.Database.ExecuteSqlCommand(sql,
                        new SqlParameter("@amountToDebit", transferAmount),
                        new SqlParameter("@outputId", outputId));
                        var inputAccount = db.InputAccounts.Find(inputId);
                        inputAccount.Balance += transferAmount;
                        db.SaveChanges();
                        trans.Commit();
                    }
                    catch (Exception ex)
                    {
                        trans.Rollback();
                    }
                }
}

 對上面的程式碼稍作解釋:首先建立了一個DbContext類的範例,然後使用這個範例通過呼叫Database.BeginTransaction()方法開啟了一個事務。該方法給我們返回了一個DbContextTransaction物件的控制程式碼,使用該控制程式碼可以提交或者回滾事務。然後使用原生SQL從OutputAccount賬戶中扣款,使用模型類給InputAccount收款。呼叫SaveChanges()方法只會影響第二個操作(在事務提交之後影響),但不會提交事務。如果兩個操作都成功了,那麼就呼叫DbContextTransaction物件的Commit()方法,否則,我們就處理異常並呼叫DbContextTransaction物件的Rollback()方法回滾事務。

四、使用已經存在的事務

有時,我們想在EF的DbContext類中使用一個已經存在的事務。原因可能有這麼幾個:

1、一些操作可能在應用的不同部分完成。

2、對老專案使用了EF,並且這個老專案使用了一個類庫,這個類庫給我們提供了事務或者資料庫連結的控制程式碼。

對於這些場景,EF允許我們在DbContext類中使用一個和事務相關聯的已存在連線。接下來,寫一個簡單的函數來模擬老專案的類庫提供控制程式碼,該函數使用純粹的ADO.NET執行扣款操作,函數定義如下:

static bool DebitOutputAccount(SqlConnection conn, SqlTransaction trans, int accountId, decimal amountToDebit)
{
            int affectedRows = 0;
            var command = conn.CreateCommand();
            command.Transaction = trans;
            command.CommandType = CommandType.Text;
            command.CommandText = "Update OutputAccounts set Balance=Balance-@amountToDebit where id=@accountId";
            command.Parameters.AddRange(new SqlParameter[]
                            {  new SqlParameter("@amountToDebit",amountToDebit),
                               new SqlParameter("@accountId",accountId)
                            });
            try
            {
                affectedRows = command.ExecuteNonQuery();
            }
            catch (Exception ex)
            {
                throw ex;
            }
            return affectedRows == 1;
}

 這種情況,我們不能使用Database.BeginTransaction()方法,因為我們需要將SqlConnection物件和SqlTransaction物件傳給該函數,並把該函數放到我們的事務裡。這樣,我們就需要首先建立一個SqlConnection,然後開始SqlTransaction,程式碼如下:

int outputId = 2, inputId = 1; decimal transferAmount = 1000m;
var connectionString = ConfigurationManager.ConnectionStrings["AppConnection"].ConnectionString;
using (var conn = new SqlConnection(connectionString))
{
                conn.Open();
                using (var trans = conn.BeginTransaction())
                {
                    try
                    {
                        var result = DebitOutputAccount(conn, trans, outputId, transferAmount);
                        if (!result)
                            throw new Exception("不能正常扣款!");
                        using (var db = new EFDbContext(conn, contextOwnsConnection: false))
                        {
                            db.Database.UseTransaction(trans);
                            var inputAccount = db.InputAccounts.Find(inputId);
                            inputAccount.Balance += transferAmount;
                            db.SaveChanges();
                        }
                        trans.Commit();
                    }
                    catch (Exception ex)
                    {
                        trans.Rollback();
                    }
                }
}

同時,需要修改資料上下文類,資料庫上下文類程式碼修改如下:

using EFTransactionApp.Model;
using System;
using System.Collections.Generic;
using System.Data.Common;
using System.Data.Entity;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace EFTransactionApp.EF
{
    //contextOwnsConnection
    //false:表示上下文和資料庫連線沒有關係,上下文釋放了,資料庫連線還沒釋放;
    //true:上下文釋放了,資料庫連線也就釋放了。
    public class EFDbContext:DbContext
    {
        //public EFDbContext()
        //    : base("name=AppConnection")
        //{

        //}

        public EFDbContext(DbConnection conn, bool contextOwnsConnection)
            : base(conn, contextOwnsConnection)
        {

        }


        public DbSet<OutputAccount> OutputAccounts { get; set; }

        public DbSet<InputAccount> InputAccounts { get; set; }
    }
}

五、選擇合適的事務管理

我們已經知道了好幾種使用EF出來事務的方法,下面一一對號入座:

1、如果只有一個DbContext類,那麼應該盡力使用EF的預設事務管理。我們總應該將所有的操作組成一個在相同的DbContext物件的作用域中執行的工作單元,SaveChanges()方法會提交處理事務。

2、如果使用了多個DbContext物件,那麼管理事務的最佳方法可能就是把呼叫放到TransactionScope物件的作用域中了。

3、如果要執行原生的SQL命令,並想把這些操作和事務關聯起來,那麼應該使用EF提供的Database.BeginTransaction()方法。然而這種方法只支援EF6以後的版本,以前的版本不支援。

4、如果想為要求SqlTransaction的老專案使用EF,那麼可以使用Database.UseTransaction()方法,在EF6中可用。

範例程式碼下載地址:點此下載

到此這篇關於Entity Framework使用Code First模式管理事務的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支援it145.com。


IT145.com E-mail:sddin#qq.com