运维开发网

使用框架CodeFirst模式管理事务

运维开发网 https://www.qedev.com 2022-08-22 14:50 出处:网络
本文详细讲解了EntityFramework使用CodeFirst模式管理事务的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 一、什么是事务

本文详细讲解了EntityFramework使用CodeFirst模式管理事务的方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下 一、什么是事务

在处理以数据为中心的应用程序时,另一个重要的主题是事务管理。ADO.NET为事务管理提供了一个非常干净有效的API。因为EF运行在ADO.NET上,所以EF可以使用ADO。NET的事务管理功能。

从数据库的角度谈事务,就是把一系列的操作当作一个不可分割的操作。所有操作要么全部成功,要么全部失败。事务的概念是一个可靠的工作单元,一个事务中的所有数据库操作都应该被视为一个工作单元。

从应用程序的角度来看,如果我们将多个数据库操作视为一个工作单元,我们应该将这些操作包装在一个事务中。为了能够使用事务,应用程序需要执行以下步骤:

1.开始交易。

2.执行所有查询和数据库操作,这些操作被视为一个工作单元。

3.如果所有事务都成功,则提交事务。

4.如果任何操作失败,事务将被回滚。

二、创建测试环境

1.说到交易,最经典的例子就是银行转账。这里我们也用这个例子来理解与事务相关的概念。为了简单地模拟银行转帐的场景,假设银行对不同的帐户使用不同的表。相应地,我们创建了两个实体类,OutputAccount和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("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 DbSetlt;OutputAccountgt; OutputAccounts { get; set; } public DbSetlt;InputAccountgt; 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 : DbMigrationsConfigurationlt;EFTransactionApp.EF.EFDbContextgt; { 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 DbSetlt;Tgt;.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事务将1000从输出帐户的张三转到输入帐户的李四。

使用EF的默认交易执行

EF的默认行为是,每当执行任何涉及创建、更新或删除的查询时,都会默认创建一个事务。当调用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。因此,这两个操作被有效地包装在一个事务中,并作为一个工作单元来执行。如果任何操作失败,数据将不会改变。

可能有人会疑惑:上面的程序执行成功了,但是没有看到交易效果。能不能修改代码让上面的程序失败然后就能看到交易效果了?答案是肯定的,请在下面修改上面的代码。

通过查看数据库的表结构,你会发现余额的数据类型是


表示余额列的最大输入长度为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对象的操作关联到一个工作单元中。此时,我们需要将SaveChanges()方法的调用包装在TransactionScope对象内。为了描述这个场景,我们使用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()方法,从而回滚更改。交易执行失败的情况也可以用上面的方式修改,使余额列的长度超过最大长度,这里不做演示。

三、使用EF6管理事务

从EF6开始,EF就提供了数据库。DbContext对象上的BeginTransaction()方法,这在使用Context类在事务中执行本机SQL命令时特别有用。

让我们来看看如何使用这种新方法来管理事务。这里,我们使用原生SQL从OutputAccount帐户中扣款,并使用model类从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类的实例,然后使用这个实例通过调用数据库来启动一个事务。BeginTransaction()方法。该方法返回DbContextTransaction对象的句柄,可用于提交或回滚事务。然后使用原生SQL从OutputAccount帐户中扣款,并使用model类从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;}

在这种情况下,我们不能使用数据库。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 DbSetlt;OutputAccountgt; OutputAccounts { get; set; } public DbSetlt;InputAccountgt; InputAccounts { get; set; } }}五、选择合适的事务管理

我们已经知道使用EF进行交易的几种方法。下面是相应的数字:

1.如果只有一个DbContext类,应该尽量使用ef默认的事务管理。我们应该始终将所有操作组合成一个工作单元,在DbContext对象的相同范围内执行,SaveChanges()方法将提交事务。

2.如果使用多个DbContext对象,管理事务的最佳方法可能是将调用放在TransactionScope对象的范围内。

3.如果您希望执行本机SQL命令,并希望将这些操作与事务相关联,则应该使用数据库。EF提供的BeginTransaction()方法。但是,该方法仅支持EF6之后的版本,不支持之前的版本。

4.如果想对需要SqlTransaction的老项目使用EF,可以使用数据库。UseTransaction()方法,该方法在EF6中可用。

示例代码下载地址:单击此处下载

关于Entity Framework使用代码优先模式管理事务的文章到此结束。

0

精彩评论

暂无评论...
验证码 换一张
取 消