了解Entity Framework中事务处理

Entity Framework 6以前,框架本身并没有提供显式的事务处理方案,在EF6中提供了事务处理的API。

所有版本的EF,只要你调用SaveChanges方法进行插入、修改或删除,EF框架会自动将该操作进行事务包装。这种方法无法对事务进行显式的控制,例如新建事务等,可能会造成事务的粒度非常大,降低效率。EF不会对查询进行事务包装。

从EF6开始,默认情况下,如果每次调用Database.ExecuteSqlCommand(),如果其不在存在于任何事务中,则会将该Command包装到一个事务中。框架提供了多种重载,允许你重写这些方法,实现事务的控制。同样,执行存储过程的ObjectContext.ExecuteFunction()方法是实现了这种机制(但是ExecuteFunction不能被重写)。这两种情况下,使用的事务隔离级别均为数据库提供的默认隔离级别,SQL Server中使用的是READ COMMITED。

有同学提供了EF6之前版本的事务方案,如下:

 1 using (BlogDbContext context =new BlogDbContext())
 2 {
 3     using (TransactionScope transaction =new TransactionScope())
 4     {
 5         context.BlogPosts.Add(blogPost);
 6         context.SaveChanges();
 7         postBody.ID = blogPost.ID;
 8         context.EntryViewCounts.Add(
 9             new EntryViewCount() { EntryID = blogPost.ID });
10         context.PostBodys.Add(postBody);
11         context.SaveChanges();
12         //提交事务
13         transaction.Complete();
14     }
15 }

其实,上面方法执行结果不会错,但是存在隐患,这样情况下,显式事务其实是多余的。所以我对这种方案持怀疑态度(没有进行内部代码的分析,有时间了分析下,希望大家拍砖)。

官方体统的解决方案为:

 1 using System.Collections.Generic;
 2 using System.Data.Entity;
 3 using System.Data.SqlClient;
 4 using System.Linq;
 5 using System.Transactions;
 6
 7 namespace TransactionsExamples
 8 {
 9     class TransactionsExample
10     {
11         static void UsingTransactionScope()
12         {
13             using (var scope = new TransactionScope(TransactionScopeOption.Required))
14             {
15                 using (var conn = new SqlConnection("..."))
16                 {
17                     conn.Open();
18
19                     var sqlCommand = new SqlCommand();
20                     sqlCommand.Connection = conn;
21                     sqlCommand.CommandText =
22                         @"UPDATE Blogs SET Rating = 5" +
23                             " WHERE Name LIKE ‘%Entity Framework%‘";
24                     sqlCommand.ExecuteNonQuery();
25
26                     using (var context =
27                         new BloggingContext(conn, contextOwnsConnection: false))
28                     {
29                         var query = context.Posts.Where(p => p.Blog.Rating > 5);
30                         foreach (var post in query)
31                         {
32                             post.Title += "[Cool Blog]";
33                         }
34                         context.SaveChanges();
35                     }
36                 }
37
38                 scope.Complete();
39             }
40         }
41     }
42 }

一般情况下,用户不需要对事务进行特殊的控制,使用EF框架默认行为即可。如果要对细节进行控制,参考下面章节:

EF6 API工作机制

EF6以前版本EF框架自己管理数据库连接,如果你自己尝试打开连接可能会抛出异常(打开一个已打开的连接会抛出异常)。由于事务必须在一个打开的连接上执行,因此要合并一系列操作到一个事务中,要么使用TractionScope,要么使用ObjectContext.Connection属性直接执行EntityConnection的Open(),并BeginTransaction()。另外,如果你在数据库底层连接上执行了事务,上面API会失败。

注意:EF6中移除了仅接受关闭连接的限制。

EF6 开始提供了:

Database.BeginTransaction() : 为用户提供一种简单易用的方案,在DbContext中启动并完成一个事务 -- 合并一系列操作到该事务中。同时使用户更方便的指定事务隔离级别。

Database.UseTransaction() : 允许DbContext使用一个EF框架外的事务。

在同一DbContext中合并一系列操作到一个事务中

Database.BeginTransaction()有两个重载方法。一个方法提供一个IsolationLevel参数,另一个无参方法使用底层数据库提供程序默认的数据库事务隔离级别。两个重载方法均返回一个DbContextTransaction对象,该对象提供Commit和Rollback方法,用于数据库底层事务的提交和回滚。

使用DbContextTransaction意味着,一旦提交或回滚事务,就要释放该对象。一种简单的方法是使用using语法,在using代码块结束时自动调用该对象的Dispose方法。

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Data.Entity;
 4 using System.Data.SqlClient;
 5 using System.Linq;
 6 using System.Transactions;
 7
 8 namespace TransactionsExamples
 9 {
10     class TransactionsExample
11     {
12         static void StartOwnTransactionWithinContext()
13         {
14             using (var context = new BloggingContext())
15             {
16                 using (var dbContextTransaction = context.Database.BeginTransaction())
17                 {
18                     try
19                     {
20                         context.Database.ExecuteSqlCommand(
21                             @"UPDATE Blogs SET Rating = 5" +
22                                 " WHERE Name LIKE ‘%Entity Framework%‘"
23                             );
24
25                         var query = context.Posts.Where(p => p.Blog.Rating >= 5);
26                         foreach (var post in query)
27                         {
28                             post.Title += "[Cool Blog]";
29                         }
30
31                         context.SaveChanges();
32
33                         dbContextTransaction.Commit();
34                     }
35                     catch (Exception)
36                     {
37                         dbContextTransaction.Rollback();
38                     }
39                 }
40             }
41         }
42     }
43 }

注意:启动一个事务需要底层数据库连接已打开。因此,如果连接未打开,调用Database.BeginTransaction()会打开连接,在其Dispose时关闭连接。

传递一个现有事务到DbContext

有时,你可能需要在同一数据库上,执行一个EF框架之外更大范围的事务,这是就需要自己打开连接并启动事务,然后通知EF框架:

1) 使用已打开的数据库连接

2) 在该连接上使用现有的事务

要实现上面的行为,你需要使用继承自DbContext的构造方法XXXContext(conn,contextOwnsConnection),其中:

conn : 是一个已存在的数据库连接

contextOwnsConnection : 是一个布尔值,指示上下文是否自己占用数据库连接。

注意:这种情况下,contextOwnsConnection必须设置为false,因为它通知EF框架,在自己使用完连接后,不要关闭它。见下面代码:

1 using (var conn = new SqlConnection("..."))
2 {
3     conn.Open();
4     using (var context = new BloggingContext(conn, contextOwnsConnection: false))
5     {
6     }
7 }

此外,你必须自己启动事务(如果你不想使用默认IsolationLevel,可以自己设置之)并让EF框架知道该连接上已经存在已启动的事务(参考下面代码的33行)。
      然后就可以直接在连接上执行数据库操作,或者在DbContext上执行,所有这些操作均在同一事务中执行,你负责提交或回滚事务,并调用DatabaseTransaction.Dispose(),最后要关闭和释放数据库连接。请参考以下代码:

 1 using System;
 2 using System.Collections.Generic;
 3 using System.Data.Entity;
 4 using System.Data.SqlClient;
 5 using System.Linq;
 6 sing System.Transactions;
 7
 8 namespace TransactionsExamples
 9 {
10      class TransactionsExample
11      {
12         static void UsingExternalTransaction()
13         {
14             using (var conn = new SqlConnection("..."))
15             {
16                conn.Open();
17
18                using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot))
19                {
20                    try
21                    {
22                        var sqlCommand = new SqlCommand();
23                        sqlCommand.Connection = conn;
24                        sqlCommand.Transaction = sqlTxn;
25                        sqlCommand.CommandText =
26                            @"UPDATE Blogs SET Rating = 5" +
27                             " WHERE Name LIKE ‘%Entity Framework%‘";
28                        sqlCommand.ExecuteNonQuery();
29
30                        using (var context =
31                          new BloggingContext(conn, contextOwnsConnection: false))
32                         {
33                             context.Database.UseTransaction(sqlTxn);
34
35                             var query =  context.Posts.Where(p => p.Blog.Rating >= 5);
36                             foreach (var post in query)
37                             {
38                                 post.Title += "[Cool Blog]";
39                             }
40                            context.SaveChanges();
41                         }
42
43                         sqlTxn.Commit();
44                     }
45                     catch (Exception)
46                     {
47                         sqlTxn.Rollback();
48                     }
49                 }
50             }
51         }
52     }
53 }

注意:

  • 你可以传递null到方法Database.UseTransaction()来清除EF框架对当前事务的记忆。如果你这样做,事务既不会提交也不会回滚。所以要谨慎使用之,除非你确实需要这样。
  • 如果EF框架已经持有一个事务,此时你传递一个事务,Database.UseTransaction()将抛出一个异常:

★ EF框架已经持有一个事务;

★ 当EF框架已经在一个TransactionScope中运行;

★ 其数据库连接对象为null (例如,无连接--通常这种情况表示事务已经完成);

★ 数据库连接对象与EF框架的数据库连接对象不匹配;

对TransactionScope的一些补充

如果你使用.net framework 4.5.1及以上版本,可以使用TransactionScope的TransactionScopeAsyncFlowOption参数提供对异步的支持:

 1 using System.Collections.Generic;
 2 using System.Data.Entity;
 3 using System.Data.SqlClient;
 4 using System.Linq;
 5 using System.Transactions;
 6
 7 namespace TransactionsExamples
 8 {
 9     class TransactionsExample
10     {
11         public static void AsyncTransactionScope()
12         {
13             using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled))
14             {
15                 using (var conn = new SqlConnection("..."))
16                 {
17                     await conn.OpenAsync();
18
19                     var sqlCommand = new SqlCommand();
20                     sqlCommand.Connection = conn;
21                     sqlCommand.CommandText =
22                         @"UPDATE Blogs SET Rating = 5" +
23                             " WHERE Name LIKE ‘%Entity Framework%‘";
24                     await sqlCommand.ExecuteNonQueryAsync();
25
26                     using (var context = new BloggingContext(conn, contextOwnsConnection: false))
27                     {
28                         var query = context.Posts.Where(p => p.Blog.Rating > 5);
29                         foreach (var post in query)
30                         {
31                             post.Title += "[Cool Blog]";
32                         }
33
34                         await context.SaveChangesAsync();
35                     }
36                 }
37             }
38         }
39     }
40 }

目前,使用TransactionScope还有一些限制:

  • 需要.NET 4.5.1及以上版本才支持异步方法;
  • 不能适用于云方案(除非你确保只有一个连接 -- 云方案不支持分布式事务);
  • 不能和Database.UseTransaction()结合使用;
  • 如果你的DDL代码存在问题(例如数据库初始化问题)或没有通过MSDTC服务来支持分布式事务,将抛出异常;

使用TransactionScope的优点:

  • 自动将本地事务升级为分布式事务:前提是你有不止一个连接到给定数据库或要组合一个连接到另一个数据库连接到同一事务(注意:你必须启动MSDTC服务以支持分布式事务)。
  • 易于编程。如果你更希望淡化对事务的关注,而非显示操作事务,使用TransactionScope将是一个更合适的选择。

随着EF6提供了Database.BeginTransaction()和Database.UseTransaction() 两个API,使用TransactionScope不在是必须的了。如果你依然使用TransactionScope,就必须留意上面限制。建议你尽可能使用新的API,而非TransactionScope。

时间: 2024-11-08 02:07:16

了解Entity Framework中事务处理的相关文章

Entity Framework 教程——Entity Framework中的实体类型

Entity Framework中的实体类型 : 在之前的章节中我们介绍过从已有的数据库中创建EDM,它包含数据库中每个表所对应的实体.在EF 5.0/6.0中,存在POCO 实体和动态代理实体两种. POCO Entity (Plain Old CLR Object): POCO类是不依赖任何框架的类型,如同其他正常的一般类型,我们称之为"Plain Old CLR Objects"(这里不知道怎么翻译,普通的CLR对象?古老的CLR对象?大概意思就是没有什么特殊的对象吧). POC

在Entity Framework 中实现继承关系映射到数据库表

继承关系映射到数据库表中有多种方式: 第一种:TPH(table-per-hiaerachy) 每一层次一张表 (只有一张表) 仅使用名为父类的类型名的一张表,它包含了各个子类的所有属性信息,使用区分列(Disciriminator column)(通常内容为子类的类型名)来区分哪一行表示什么类型的数据. 第二种:TPT(Table-per-type) 每种类型都有一张表(父类及每个子类都有表) 父类.各子类各自都有一张表.父类的表中只有共同的数据,子类表中有子类特定的属性.TPT很像类的继承结

在Linq to sql 和 Entity framework 中使用lambda表达式实现left join

我们知道lambda表达式在Linq to sql 和 Entity framework 中使用join函数可以实现inner join,那么怎么才能在lambda表达式中实现left join呢?秘诀就是在join后面加上一个函数DefaultIfEmpty函数,实际上这个函数在linq中貌似也只有将inner join转换为left join的作用,示例如下 var joinResult = DB.Table1s.Join(DB.Table2s, a => a.id, b => b.id,

Entity Framework 中Decimal字段长度设置方法

在创建项目DbContext时,重写DbContext.OnModelCreating()方法:然后通过如下方法指定精度 1 protected override void OnModelCreating(DbModelBuilder modelBuilder) 2 { 3 modelBuilder.Entity<Product>().Property(product => product.Price).HasPrecision(18, 12); 4 } Entity Framework

在Entity Framework中使用事务

小分享:我有几张阿里云优惠券,用券购买或者升级阿里云相应产品最多可以优惠五折!领券地址:https://promotion.aliyun.com/ntms/act/ambassador/sharetouser.html?userCode=ohmepe03 继续为想使用Entity Framework的朋友在前面探路,分享的东西虽然技术含量不高,但都是经过实践检验的. 在Entity Framework中使用事务很简单,将操作放在TransactionScope中,并通过Complete()方法提

解决Entity Framework中DateTime类型字段异常

今天把一个使用了Entity Framework的程序从MySql迁移到SqlServer时,发现运行时报了一个异常: System.Data.SqlClient.SqlException: 从 datetime2 数据类型到 datetime 数据类型的转换产生一个超出范围的值. 在网上查找了一下,具体的错误原因是:C#中的DateTime类型比SqlServer中的datetime范围大.SqlServer的datetime有效范围是1753年1月1日到9999年12月31日,如果超出这个范

关于Entity Framework中的Attached报错的完美解决方案终极版

之前发表过一篇文章题为<关于Entity Framework中的Attached报错的完美解决方案>,那篇文章确实能解决单个实体在进行更新.删除时Attached的报错,注意我这里说的单个实体,指的是要更新或删除的实体不包含其它实体(比如导航属性就包含其它实体),也就是简单POCO对象:但如果不是呢?那么那篇文章里的方法在一定程度上不起作用了,仍会报错,我开始也想不明白,明明通过IsAttached函数判断要更新的实体并未Attached,但进行Attaching时但仍然报错说有相同Key,开

Entity Framework中对存储过程的返回值的处理

很早就开始注意到EF了,但一直没有机会用,换了工作后,第一个项目就使用EF6进行开发. 项目不是很大,EF完全可以胜任. 但是开发过程中,难免还是会遇到一些复杂的运算,需要频繁访问数据库. 此时,想到的比较简单的方式,就是使用存储过程,在存储过程中进行一定的运算,然后把运算的结果(一个查询结果)通过存储过程返回. 思路上完全没有问题,就直接建了一个存储过程,然后更新edmx.发现,更新后,EF中,生成的对这个存储过程的调用,返回结果却是“Int”??? 之前也有做过通过存储过程返回值,都是返回一

C# Entity Framework中的IQueryable和IQueryProvider详解

前言 相信大家对 Entity Framework 一定不陌生,我相信其中Linq To Sql是其最大的亮点之一,但是我们一直使用到现在却不曾明白内部是如何实现的,今天我们就简单的介绍IQueryable和IQueryProvider. IQueryable接口 我们先聊聊这个接口,因为我们在使用EF中经常看到linq to sql语句的返回类型是 IQueryable ,我们可以看下这个接口的结构: 代码如下: public interface IQueryable : IEnumerabl