Entity Framework使用Code First模式管理事務
一、什么是事務
處理以數(shù)據(jù)為中心的應用時,另一個重要的話題是事務管理。ADO.NET為事務管理提供了一個非常干凈和有效的API。因為EF運行在ADO.NET之上,所以EF可以使用ADO.NET的事務管理功能。
當從數(shù)據(jù)庫角度談論事務時,它意味著一系列操作被當作一個不可分割的操作。所有的操作要么全部成功,要么全部失敗。事務的概念是一個可靠的工作單元,事務中的所有數(shù)據(jù)庫操作應該被看作是一個工作單元。
從應用程序的角度來看,如果我們有多個數(shù)據(jù)庫操作被當作一個工作單元,那么應該將這些操作包裹在一個事務中。為了能夠使用事務,應用程序需要執(zhí)行下面的步驟:
1、開始事務。
2、執(zhí)行所有的查詢,執(zhí)行所有的數(shù)據(jù)庫操作,這些操作被視為一個工作單元。
3、如果所有的事務成功了,那么提交事務。
4、如果任何一個操作失敗,就回滾事務。
二、創(chuàng)建測試環(huán)境
1、提到事務,最經(jīng)典的例子莫過于銀行轉賬了。我們這里也使用這個例子來理解一下和事務相關的概念。為了簡單模擬銀行轉賬的情景,假設銀行為不同的賬戶使用了不同的表,對應地,我們創(chuàng)建了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、定義數(shù)據(jù)上下文類
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、使用數(shù)據(jù)遷移生成數(shù)據(jù)庫,并填充種子數(shù)據(jù)
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.
// 填充種子數(shù)據(jù)
context.InputAccounts.AddOrUpdate(
new InputAccount()
{
Name = "李四",
Balance = 0M
}
);
context.OutputAccounts.AddOrUpdate(
new OutputAccount()
{
Name="張三",
Balance=10000M
}
);
}
}
}4、運行程序
從應用程序的角度看,無論何時用戶將錢從OutputAccount轉入InputAccount,這個操作應該被視為一個工作單元,永遠不應該發(fā)生OutputAccount的金額扣除了,而InputAccount的金額沒有增加。接下來我們就看一下EF如何管理事務。
運行程序前,先查看數(shù)據(jù)庫數(shù)據(jù):

現(xiàn)在,我們嘗試使用EF的事務從OutputAccount的張三轉入1000給InputAccount的李四。
使用EF默認的事務執(zhí)行
EF的默認行為是:無論何時執(zhí)行任何涉及Create,Update或Delete的查詢,都會默認創(chuàng)建事務。當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();
}
}
}
}運行程序后,會發(fā)現(xiàn)數(shù)據(jù)庫中數(shù)據(jù)發(fā)生了改變:

可以看到,用戶李四的賬戶上面多了1000,用戶張三的賬戶上面少了1000。因此,這兩個操作有效地被包裹在了一個事務當中,并作為一個工作單元執(zhí)行。如果任何一個操作失敗,數(shù)據(jù)就不會發(fā)生變化。
可能有人會疑惑:上面的程序執(zhí)行成功了,沒有看到事務的效果,能不能修改一下代碼讓上面的程序執(zhí)行失敗然后可以看到事務的效果呢?答案是肯定可以的,下面將上面的代碼進行修改。
通過查看數(shù)據(jù)庫表結構會發(fā)現(xiàn)Balance的數(shù)據(jù)類型是

意味著Balance列的最大可輸入長度是16位(最大長度18位減去2位小數(shù)點),如果輸入的長度大于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();
}
}
}
}在次運行程序,會發(fā)現(xiàn)程序報錯了:

這時在查看數(shù)據(jù)庫,發(fā)現(xiàn)用戶張三的余額還是9000沒有發(fā)生變化,說明事務起作用了。
5、使用TransactionScope處理事務
如果有一個場景具有多個DbContext對象,那么我們想將涉及多個DbContext對象的操作關聯(lián)為一個工作單元,這時,我們需要在TransactionScope對象內部包裹SaveChanges()方法的調用。為了描述這個場景,我們使用DbContext類的兩個不同實例來執(zhí)行扣款和收款,代碼如下:
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實例來執(zhí)行扣款和收款操作。因此,默認的EF行為不會工作。在調用各自的SaveChanges()方法時,和上下文相關的各個事務不會提交。相反,因為它們都在 TransactionScope對象的內部,所以,當TransactionScope對象的Complete()方法調用時,事務才會提交。如果任何一個操作失敗,就會發(fā)生異常,TransactionScope就不會調用Complete()方法,從而回滾更改。事務執(zhí)行失敗的案例也可以按照上面的方式進行修改,使Balance列的長度超過最大長度,這里就不在演示了。
三、使用EF6管理事務
從EF6開始,EF在DbContext對象上提供了Database.BeginTransaction()方法,當使用上下文類在事務中執(zhí)行原生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();
}
}
}對上面的代碼稍作解釋:首先創(chuàng)建了一個DbContext類的實例,然后使用這個實例通過調用Database.BeginTransaction()方法開啟了一個事務。該方法給我們返回了一個DbContextTransaction對象的句柄,使用該句柄可以提交或者回滾事務。然后使用原生SQL從OutputAccount賬戶中扣款,使用模型類給InputAccount收款。調用SaveChanges()方法只會影響第二個操作(在事務提交之后影響),但不會提交事務。如果兩個操作都成功了,那么就調用DbContextTransaction對象的Commit()方法,否則,我們就處理異常并調用DbContextTransaction對象的Rollback()方法回滾事務。
四、使用已經(jīng)存在的事務
有時,我們想在EF的DbContext類中使用一個已經(jīng)存在的事務。原因可能有這么幾個:
1、一些操作可能在應用的不同部分完成。
2、對老項目使用了EF,并且這個老項目使用了一個類庫,這個類庫給我們提供了事務或者數(shù)據(jù)庫鏈接的句柄。
對于這些場景,EF允許我們在DbContext類中使用一個和事務相關聯(lián)的已存在連接。接下來,寫一個簡單的函數(shù)來模擬老項目的類庫提供句柄,該函數(shù)使用純粹的ADO.NET執(zhí)行扣款操作,函數(shù)定義如下:
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對象傳給該函數(shù),并把該函數(shù)放到我們的事務里。這樣,我們就需要首先創(chuàng)建一個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();
}
}
}同時,需要修改數(shù)據(jù)上下文類,數(shù)據(jù)庫上下文類代碼修改如下:
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:表示上下文和數(shù)據(jù)庫連接沒有關系,上下文釋放了,數(shù)據(jù)庫連接還沒釋放;
//true:上下文釋放了,數(shù)據(jù)庫連接也就釋放了。
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; }
}
}五、選擇合適的事務管理
我們已經(jīng)知道了好幾種使用EF出來事務的方法,下面一一對號入座:
1、如果只有一個DbContext類,那么應該盡力使用EF的默認事務管理。我們總應該將所有的操作組成一個在相同的DbContext對象的作用域中執(zhí)行的工作單元,SaveChanges()方法會提交處理事務。
2、如果使用了多個DbContext對象,那么管理事務的最佳方法可能就是把調用放到TransactionScope對象的作用域中了。
3、如果要執(zhí)行原生的SQL命令,并想把這些操作和事務關聯(lián)起來,那么應該使用EF提供的Database.BeginTransaction()方法。然而這種方法只支持EF6以后的版本,以前的版本不支持。
4、如果想為要求SqlTransaction的老項目使用EF,那么可以使用Database.UseTransaction()方法,在EF6中可用。
示例代碼下載地址:點此下載
到此這篇關于Entity Framework使用Code First模式管理事務的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持腳本之家。
- Entity?Framework代碼優(yōu)先(Code?First)模式
- Entity Framework使用Code First模式管理存儲過程
- Entity Framework使用Code First模式管理視圖
- Entity?Framework使用Code?First的實體繼承模式
- Entity Framework使用Code First模式管理數(shù)據(jù)庫
- EF使用Code First模式生成單數(shù)形式表名
- EF使用Code First模式給實體類添加復合主鍵
- 使用EF的Code?First模式操作數(shù)據(jù)庫
- C#筆記之EF Code First 數(shù)據(jù)模型 數(shù)據(jù)遷移
- Entity?Framework代碼優(yōu)先Code?First入門
相關文章
asp.net后臺cs中的JSON格式變量在前臺Js中調用方法(前后臺示例代碼)
本文主要介紹下asp.net后臺cs中的JSON格式變量在前臺Js中調用方法,下面是前后臺的實現(xiàn)代碼,感興趣的朋友可以參考下哈,下對大家有所幫助2013-06-06
asp.net 合并GridView中某列相同信息的行(單元格)
合并GridView中某列相同信息的行(單元格)2009-11-11

