一、什么是事务

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

当从数据库角度谈论事务时,它意味着一系列操作被当作一个不可分割的操作。所有的操作要么全部成功,要么全部失败。事务的概念是一个可靠的工作单元,事务中的所有数据库操作应该被看作是一个工作单元。

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

1、开始事务。

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

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

4、如果任何一个操作失败,就回滚事务。

二、创建测试环境

提到事务,最经典的例子莫过于银行转账了。我们这里也使用这个例子来理解一下和事务相关的概念。为了简单模拟银行转账的情景,假设银行为不同的账户使用了不同的表,对应地,我们创建了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()]
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()]
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 = , inputId = ;
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 = , inputId = ;
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*;
//4 提交事务
db.SaveChanges();
} }
}
}

在次运行程序,会发现程序报错了:

这时在查看数据库,发现用户张三的余额还是9000没有发生变化,说明事务起作用了。

5、使用TransactionScope处理事务

如果有一个场景具有多个DbContext对象,那么我们想将涉及多个DbContext对象的操作关联为一个工作单元,这时,我们需要在TransactionScope对象内部包裹SaveChanges()方法的调用。为了描述这个场景,我们使用DbContext类的两个不同实例来执行扣款和收款,代码如下:

int outputId = , inputId = ;
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 = , inputId = ; 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 = ;
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 == ;
}

这种情况,我们不能使用Database.BeginTransaction()方法,因为我们需要将SqlConnection对象和SqlTransaction对象传给该函数,并把该函数放到我们的事务里。这样,我们就需要首先创建一个SqlConnection,然后开始SqlTransaction,代码如下:

int outputId = , inputId = ; 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中可用。

示例代码下载地址:https://pan.baidu.com/s/1c2wGgSS

Entity Framework应用:使用Code First模式管理事务的更多相关文章

  1. Entity Framework应用:Code First模式数据迁移的基本用法

    使用Entity Framework的Code First模式在进行数据迁移的时候会遇到一些问题,熟记一些常用的命令很重要,下面整理出了数据迁移时常用的一些命令. 一.模型设计 EF默认使用id字段作 ...

  2. Entity Framework应用:使用Code First模式管理视图

    一.什么是视图 视图在RDBMS(关系型数据库管理系统)中扮演了一个重要的角色,它是将多个表的数据联结成一种看起来像是一张表的结构,但是没有提供持久化.因此,可以将视图看成是一个原生表数据顶层的一个抽 ...

  3. 学习Entity Framework 中的Code First

    这是上周就写好的文章,是在公司浩哥的建议下写的,本来是部门里面分享求创新用的,这里贴出来分享给大家. 最近在对MVC的学习过程中,接触到了Code First这种新的设计模式,感觉很新颖,并且也体验到 ...

  4. 转载:学习Entity Framework 中的Code First

    看完觉得不错,适合作为学习资料,就转载过来了 原文链接:http://www.cnblogs.com/Wayou/archive/2012/09/20/EF_CodeFirst.html 这是上周就写 ...

  5. Entity Framework 6.x Code Frist For Oracle 实践与注意点

    Entity Framework 6.x Code Frist For Oracle 实践与注意点 开发环境 Visual Studio.net 2015/2017 Oracle 11g/12c 数据 ...

  6. Entity Framework工具POCO Code First Generator的使用

    在使用Entity Framework过程中,有时需要借助工具生成Code First的代码,而Entity Framework Reverse POCO Code First Generator是一 ...

  7. Entity Framework工具POCO Code First Generator的使用(参考链接:https://github.com/sjh37/EntityFramework-Reverse-POCO-Code-First-Generator)

    在使用Entity Framework过程中,有时需要借助工具生成Code First的代码,而Entity Framework Reverse POCO Code First Generator是一 ...

  8. Entity Framework应用:Code First的实体继承模式

    Entity Framework的Code First模式有三种实体继承模式 1.Table per Type (TPT)继承 2.Table per Class Hierarchy(TPH)继承 3 ...

  9. Entity Framework 6.0 Code First(转)

    源自:http://www.cnblogs.com/panchunting/tag/Code%20First/ 1 Conventions 2 Custom Conventions 3 Data An ...

随机推荐

  1. Android 数据加密算法 Des,Base64详解

    一,DES加密: 首先网上搜索了一个DES加密算法工具类: import java.security.*;import javax.crypto.*; public class DesHelper { ...

  2. NGUI: UIPanel控件

    转自:http://blog.csdn.net/huang9012/article/details/19411703 http://bbs.9ria.com/thread-417659-1-1.htm ...

  3. SpringSecurityFilter 链

    1. HttpSessionContextIntegrationFilter 位于过滤器顶端,第一个起作用的过滤器. 用途一,在执行其他过滤器之前,率先判断用户的session中是否已经存在一个Sec ...

  4. spring 中 AOP 功能

    1 PointCut 由 ClassFilter 和 MethodMatcher 构成,通过 ClassFilter 定位到类上,通过 MethodMatcher 定位到方法. 2 Spring 支持 ...

  5. 模拟HTTP请求获取返回值

    package org.jeecgframework.core.util; import java.io.BufferedReader; import java.io.IOException; imp ...

  6. wavwrite注意事项

    前几天群里有人提出一个问题:MATLAB里,同样频率的信号写入/读取,为什么频率感觉不同? 测试code: fs = 2000; f0 = 20; t = 0:1/fs:1; subplot 211 ...

  7. 理解:Before和:After伪元素

    CSS样式表的主要作用是修饰Web页面上的HTML标记,但有时候,为了实现某个效果而往页面里反复添加某个HTML标记很繁琐,或者是显得多余,或者是由于某种原因而做不到.这就是CSS伪元素(Pseudo ...

  8. javascript Set data structures

    集合(set)是一组无序的,但彼此之间又有一定相关性的数据集.每个成员在数组中只能出现一次. 在使用集合(set)之前最好先理解一下内容: 1.不包含任何成员的集合称为空集合. 2.如果两个集合的成员 ...

  9. Eclipse使用git最基本流程(提交远程仓库的方法)

    注册一个github账号 注册完成之后,点击右上角的settings(就是那个齿轮,设置的图标) Step6 Egit的使用 首先,登入你的github账号,点击加号,选择New repositror ...

  10. jQuery $.extend()使用方法

    $.extend()使用方法总结. jQuery为开发插件提拱了两个方法,各自是: jQuery.fn.extend(object); jQuery.extend(object); jQuery.ex ...