What?
首先,說一下什么是EF Core中的Transaction
Transaction允許以原子方式處理多個數據庫操作,如果事務已提交,則所有操作都應用於數據庫,如果事務回滾,則沒有任何操作應用於數據庫。
所謂原子方式 是指對數據庫的每一個操作是對立開來的,但是多個操作能合成一個整體(個人理解)。
當操作到某一步失敗了,那么會觸發事物的回滾,把前面成功的操作也進行撤銷,為什么這一操作這么重要呢?我舉個例子你就知道了
就那拿一行轉賬這件事情來說。正常的A給B轉賬X元有兩步:
1. 從A的賬戶余額中減去X元。
2. 往B的銀行賬戶中添加X元。
假如,第一步執行完了,第二部因為某種原因執行失敗了,那么,是不是A的賬戶平白無故地少了X元而B並沒有多X元呢?顯然這種事情是不能發生的,正確的做法是,把第一步撤銷,即把A賬戶減去的X元加上。
然而在在.Net中,如果你使用EF Core來操作數據庫,這些都不用我們手動完成了,EF Core的事物完全可以幫我們完成這樣的操作。
How?
下面我們利用一個asp.net core webapi的例子來講解EF Core中這種Transaction的用法。
新建一個webapi應用程序
選擇Asp.NET Core Web應用程序
.選擇WebApi
搭建EF Core
創建Model文件夾和BankContext數據庫上下文,Walet錢包實體,如圖:
Wallet的代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace EFCoreRollback.Models { public class Wallet { public int Id { get; set; } public string Name { get; set; } public double Money { get; set; } } }
BankContext的代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; namespace EFCoreRollback.Models { public class BankContext:DbContext { public BankContext(DbContextOptions<BankContext> options) : base(options) { } public DbSet<Wallet> Wallets { get; set; } } }
因為我是用Mysql數據庫進行數據存儲的,所以需要添加Mysql的EF Core引用,選中依賴項,右鍵菜單 選擇管理Nuget程序包,
安裝下列引用項目(Pomelo.EntityFrameworkCore.MySql):
在appsettings.json中加入數據庫連接字符串,如下:
"ConnectionStrings": { "Connection": "Data Source=127.0.0.1;Database=bank;User ID=root;Password=123456;" }
修改statup.cs,進行BankContext的依賴注入,主要修改了灰色部分,代碼如下:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EFCoreRollback.Models; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; namespace EFCoreRollback { public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) {var connectString = Configuration.GetConnectionString("Connection" ); services.AddDbContext<BankContext>(options =>
{ options.UseMySql(connectString); options.UseLoggerFactory(
new LoggerFactory().AddConsole()); //加入該句會把EF Core執行過程中的Sql語句在控制台輸出
}); services.AddMvc(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseMvc(); } } }
創建數據庫和表
打開NuGet報管理器下的程序包管理控制台
先后執行以下兩條語句
Add-Migrition Init
Updata-Database
執行效果如圖:
執行成功后,Mysql數據庫中多了Bank數據庫和walets表,如圖:
添加控制器(業務代碼)
在Controllers下新建一個BankController.cs,完整代碼如下(核心部分為灰色背景):
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using EFCoreRollback.Models; using Microsoft.AspNetCore.Mvc; namespace EFCoreRollback.Controllers { public class BankController : Controller { private readonly BankContext _bankContext; public BankController(BankContext context) { _bankContext = context; } /// <summary> /// 數據初始化 /// </summary> [HttpGet] [Route("bangk/InitData")] public string InitData() { if (_bankContext.Wallets.ToList().Count == 0) { Wallet AUser = new Wallet() { Name = "A", Money = 100 }; Wallet BUser = new Wallet() { Name = "B", Money = 100 }; _bankContext.Wallets.Add(AUser); _bankContext.Wallets.Add(BUser); _bankContext.SaveChanges(); } return "Success"; } /// <summary> /// 進行轉賬 /// </summary> /// <returns></returns> [HttpGet] [Route("bank/TransferAccounts")] public string TransferAccounts() {using (var transaction = _bankContext.Database.BeginTransaction()) { try
{ AAction(); BAction();
//如果未執行到Commit()就執行失敗遇到異常了,EF Core會自動進行數據回滾(前提是使用Using)
transaction.Commit(); } catch
(Exception ex) { // TODO: Handle failure return
ex.Message; } } return "success"
; } /// <summary> /// 從A的賬戶里面減掉10元 /// </summary> private void AAction() { var AUser = _bankContext.Wallets.Where(u => u.Name == "A").FirstOrDefault(); AUser.Money -= 10; _bankContext.SaveChanges(); } /// <summary> /// 從B的賬戶里面加上10元 /// </summary> private void BAction() { var BUser = _bankContext.Wallets.Where(u => u.Name == "B").FirstOrDefault(); BUser.Money += 10; throw new Exception("B的數據在保存前出現異常了"); //使用該方法模擬出現數據保存異常 _bankContext.SaveChanges(); } /// <summary> /// 展示錢包賬戶 /// </summary> /// <returns></returns> [HttpGet] [Route("bank/Show")] public List<Wallet> ShowWallets() { return _bankContext.Wallets.ToList(); } } }
通過InitData方法,我們把數據初始化,往數據庫中插入A、B用戶,他們錢包的初始金額都為100元。
通過TransferAccounts方法,我們執行轉賬操作,通過using引入了EF Core的Transaction,如果未執行到Commit()就執行失敗遇到異常了,EF Core會自動進行數據回滾(前提是使用Using)。
在執行AAction后,執行BAction,其中BAction在數據保存前,設置了一個異常。
執行接口(調用業務)
首先,其啟動方式從IIS切換到WebAPi程序本身,為的是在控制台中看到輸出的SQL語句。
程序成功啟動后,我們調用數據初始化接口,效果如圖:
有了數據后,我們調用轉賬接口進行轉賬操作,如圖:
進行轉賬操作,在A的賬戶成功減掉10元后,在B的賬戶加上10元保存時,由於我們設置了異常,程序跳出了。
如果按照我們正常的思維方式,因為B在保存數據前異常了,所以最終結果因該是:A的賬戶少了10元,而B的賬戶金額未變。事實是不是這樣呢?
我們執行Show接口,展示A和B用戶的錢包金額情況,可以看到,A和B的錢包金額都是100,
why?
為什么A的賬戶明明執行了減去10元的操作,而最后沒有生效呢?原來是在執行transaction.Commit()之前,程序遇到異常了,它會自動調用transaction.Rollback()進行數據回滾,撤銷A的減去10元這一操作。
Benefit?
使用EF Core的Transaction要么所有操作全部成功,要么一個操作都不執行,可以保護數據安全。
該項目的完整代碼:https://github.com/liuzhenyulive/EFCoreTransaction
如果您覺得有幫助,請點擊推薦,謝謝。