EF Core利用Transaction对数据进行回滚保护
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
如果您觉得有帮助,请点击推荐,谢谢。