Transaction之EF

了解Entity Framework中事务处理

 

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

 

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

 

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

 

using (BlogDbContext context =new BlogDbContext())
{
    using (TransactionScope transaction =new TransactionScope())
    {
        context.BlogPosts.Add(blogPost);
        context.SaveChanges();
        postBody.ID = blogPost.ID;
        context.EntryViewCounts.Add(
            new EntryViewCount() { EntryID = blogPost.ID });
        context.PostBodys.Add(postBody);
        context.SaveChanges();
        //提交事务
        transaction.Complete();
    } 
}

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

 

  官方体统的解决方案为:

using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Transactions; 
 
namespace TransactionsExamples 
{ 
    class TransactionsExample 
    { 
        static void UsingTransactionScope() 
        { 
            using (var scope = new TransactionScope(TransactionScopeOption.Required)) 
            { 
                using (var conn = new SqlConnection("...")) 
                { 
                    conn.Open(); 
 
                    var sqlCommand = new SqlCommand(); 
                    sqlCommand.Connection = conn; 
                    sqlCommand.CommandText = 
                        @"UPDATE Blogs SET Rating = 5" + 
                            " WHERE Name LIKE '%Entity Framework%'"; 
                    sqlCommand.ExecuteNonQuery(); 
 
                    using (var context = 
                        new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        var query = context.Posts.Where(p => p.Blog.Rating > 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
                        context.SaveChanges(); 
                    } 
                } 
 
                scope.Complete(); 
            } 
        } 
    } 
}

  一般情况下,用户不需要对事务进行特殊的控制,使用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方法。

using System; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Transactions; 
 
namespace TransactionsExamples 
{ 
    class TransactionsExample 
    { 
        static void StartOwnTransactionWithinContext() 
        { 
            using (var context = new BloggingContext()) 
            { 
                using (var dbContextTransaction = context.Database.BeginTransaction()) 
                { 
                    try 
                    { 
                        context.Database.ExecuteSqlCommand( 
                            @"UPDATE Blogs SET Rating = 5" + 
                                " WHERE Name LIKE '%Entity Framework%'" 
                            ); 
 
                        var query = context.Posts.Where(p => p.Blog.Rating >= 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
 
                        context.SaveChanges(); 
 
                        dbContextTransaction.Commit(); 
                    } 
                    catch (Exception) 
                    { 
                        dbContextTransaction.Rollback(); 
                    } 
                } 
            } 
        } 
    } 
}

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

 

传递一个现有事务到DbContext

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

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

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

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

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

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

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

using (var conn = new SqlConnection("...")) 
{ 
    conn.Open(); 
    using (var context = new BloggingContext(conn, contextOwnsConnection: false)) 
    { 
    } 
}

  

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

using System; 
using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
sing System.Transactions; 
 
namespace TransactionsExamples 
{ 
     class TransactionsExample 
     { 
        static void UsingExternalTransaction() 
        { 
            using (var conn = new SqlConnection("...")) 
            { 
               conn.Open(); 
 
               using (var sqlTxn = conn.BeginTransaction(System.Data.IsolationLevel.Snapshot)) 
               { 
                   try 
                   { 
                       var sqlCommand = new SqlCommand(); 
                       sqlCommand.Connection = conn; 
                       sqlCommand.Transaction = sqlTxn; 
                       sqlCommand.CommandText = 
                           @"UPDATE Blogs SET Rating = 5" + 
                            " WHERE Name LIKE '%Entity Framework%'"; 
                       sqlCommand.ExecuteNonQuery(); 
 
                       using (var context =  
                         new BloggingContext(conn, contextOwnsConnection: false)) 
                        { 
                            context.Database.UseTransaction(sqlTxn); 
 
                            var query =  context.Posts.Where(p => p.Blog.Rating >= 5); 
                            foreach (var post in query) 
                            { 
                                post.Title += "[Cool Blog]"; 
                            } 
                           context.SaveChanges(); 
                        } 
 
                        sqlTxn.Commit(); 
                    } 
                    catch (Exception) 
                    { 
                        sqlTxn.Rollback(); 
                    } 
                } 
            } 
        } 
    } 
}

  

注意:

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

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

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

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

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

 

对TransactionScope的一些补充

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

using System.Collections.Generic; 
using System.Data.Entity; 
using System.Data.SqlClient; 
using System.Linq; 
using System.Transactions; 
 
namespace TransactionsExamples 
{ 
    class TransactionsExample 
    { 
        public static void AsyncTransactionScope() 
        { 
            using (var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) 
            { 
                using (var conn = new SqlConnection("...")) 
                { 
                    await conn.OpenAsync(); 
 
                    var sqlCommand = new SqlCommand(); 
                    sqlCommand.Connection = conn; 
                    sqlCommand.CommandText = 
                        @"UPDATE Blogs SET Rating = 5" + 
                            " WHERE Name LIKE '%Entity Framework%'"; 
                    await sqlCommand.ExecuteNonQueryAsync(); 
 
                    using (var context = new BloggingContext(conn, contextOwnsConnection: false)) 
                    { 
                        var query = context.Posts.Where(p => p.Blog.Rating > 5); 
                        foreach (var post in query) 
                        { 
                            post.Title += "[Cool Blog]"; 
                        } 
 
                        await context.SaveChangesAsync(); 
                    } 
                } 
            } 
        } 
    } 
}

  

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

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

 

使用TransactionScope的优点:

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

 

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

 

 

 

.NET开发中的事务处理大比拼 之 System.Transactions

通过System.Transactions,则只要简单的几行代码,不需要继承,不需要Attribute标记。用户根本不需要考虑是简单事务还是分布式事务。新模型会自动根据事务中涉及的对象资源判断使用何种事务管理器

简而言之,对于任何的事务,用户只要使用同一种方法进行处理即可

 

首先要引用:using System.Transactions;
其次,将事务操作代码放在TransactionScope中执行。如:

using (TransactionScope ts = new TransactionScope())
{
    //事务操作代码
    ts.Complete();
}

  这是最简单,也是最常见的用法。创建了新的 TransactionScope 对象后,即开始创建事务范围。如代码示例所示,建议使用 using 语句创建范围。位于 using 块内的所有操作将成为一个事务的一部分,因为它们共享其所定义的事务执行上下文。本例中的最后一行,调用 TransactionScope  Complete 方法,将导致退出该块时请求提交该事务。此方法还提供了内置的错误处理,出现异常时会终止事务。        

 

 

 

using (TransactionScope ts = new TransactionScope())//使整个代码块成为事务性代码
{
    #region 在这里编写需要具备Transaction的代码
    string msg = "";
    string conString = "data source=127.0.0.1;database=codematic;user id=sa;
password=";
    SqlConnection myConnection = new SqlConnection(conString);
    myConnection.Open();
    SqlCommand myCommand = new SqlCommand();
    myCommand.Connection = myConnection;
    try
    {
        myCommand.CommandText = "update P_Product set Name='电脑2' where Id=52";
        myCommand.ExecuteNonQuery();
        myCommand.CommandText = "update P_Product set Name='电脑3' where Id=53";
        myCommand.ExecuteNonQuery();
        msg = "成功!";
    }
    catch (Exception ex)
    {
        msg = "失败:" + ex.Message;
    }
    finally
    {
        myConnection.Close();
    }
    #endregion
    ts.Complete();
    return msg;               
}            

 

  上面的代码演示了在一个Transaction Scope里面打开一个数据库连接的过程。这个数据库连接由于处在一个Transaction Scope里面,所以会自动获得Transaction的能力。如果这里数据库连接的是SQL Server 2005,那么这个Transaction将不会激活一个MSDTC管理的分布式事务,而是会由.NET创建一个Local Transaction,性能非常高。但是如果是SQL Server 2000,则会自动激活一个分布式事务,在性能上会受一定的损失。

 

 

void MethodMoreConn()
{
    using (TransactionScope ts = new TransactionScope())
    {
        using (SqlConnection conn = new SqlConnection(conString1))
        {
            conn.Open();
            using (SqlConnection conn2 = new SqlConnection(conString2))
            {
                conn2.Open();
            }
        }
        ts.Complete();
    } 
}

  这个例子更加充分地说明了Transaction Scope的强大,两个数据库连接!虽然上面的connconn2是两个不同的连接对象,可能分别连接到不同的数据库,但是由于它们处在一个TransactionScope中,它们就具备了“联动”的Transaction能力。在这里,将自动激活一个MSDTC管理的分布式事务(可以通过打开【管理工具】里面的组件服务,来查看当前的分布式事务列表)。

 

 

在分布式事务中登记ADO.NET 2.0 中的新增功能支持使用 EnlistTransaction 方法在分布式事务中登记。由于 EnlistTransaction  Transaction 实例中登记连接,因此,该方法利用 System.Transactions 命名空间中的可用功能来管理分布式事务,从而比使用 System.EnterpriseServices. ITransaction 对象的 EnlistDistributedTransaction 更可取。

此外,其语义也稍有不同:在一个事务中显式登记了某个连接后,如果第一个事务尚未完成,则无法取消登记或在另一个事务中登记该连接。

void MethodEnlist()
{
    CommittableTransaction tx = new CommittableTransaction();
    using (SqlConnection conn = new SqlConnection(conString))
    {
        conn.EnlistTransaction(tx);
    }
    tx.Commit();
}

  

 

 

实现嵌套事务范围void RootMethod()

{
    using (TransactionScope scope = new TransactionScope())
    {
        //操作代码
        SonMethod();//子事务方法
        scope.Complete();
    }
}
void SonMethod()
{
    using (TransactionScope scope = new TransactionScope())
    {
        //操作代码
        scope.Complete();
    }
}

 

 

 

事务范围附加选项   如果你想要保留代码部分执行的操作,并且在操作失败的情况下不希望中止环境事务,则Suppress对你很有帮助。例如,在你想要执行日志记录或审核操作时,不管你的环境事务是提交还是中止,上述值都很有用。该值允许你在事务范围内具有非事务性的代码部分,如以下示例所示。

void MethodSuppress()
{
    using (TransactionScope scope1 = new TransactionScope())//开始事务
    {
        try
        {
            //开始一个非事务范围 
            using (TransactionScope scope2 = new TransactionScope(
               TransactionScopeOption.Suppress))
            {
                //不受事务控制代码
            }
            //从这里开始又回归事务处理
        }
        catch
        { }        
    }
}

  

 

 

虽然.NET 2.0对事务提供了很好的支持,但是没有必要总是使用事务。

使用事务的第一条规则是,在能够使用事务的时候都应该使用事务,但是不要使用过度

原因在于,每次使用事务都会占用一定的开销。

另外,事务可能会锁定一些表的行。

还有一条规则是,只有当操作需要的时候才使用事务

例如,如果只是从数据库中查询一些记录,或者执行单个查询,则在大部分时候都不需要使用显式事务。

 

开发人员应该在头脑中始终保持一个概念,就是用于修改多个不同表数据的冗长事务会严重妨碍系统中的所有其他用户

这很可能导致一些性能问题。

 

当实现一个事务时,遵循下面的实践经验能够达到可接受的结果:
l  避免使用在事务中的Select返回数据,除非语句依赖于返回数据。
l  如果使用Select语句,则只选择需要的行,这样不会锁定过多的资源,而尽可能地提高性能。
l  尽量将事务全部写在T-SQL或者API中。
l  避免事务与多重独立的批处理工作结合,应该将这些批处理放置在单独的事务中。
l  尽可能避免大量更新。 


另外,必须注意的一点就是事务的默认行为

在默认情况下,如果没有显式地提交事务,则事务会回滚

虽然默认行为允许事务的回滚,但是显式回滚方法总是一个良好的编程习惯。

这不仅仅只是释放锁定数据,也将使得代码更容易读取并且更少错误。


      .NET提供的事务功能很强大,具体的内容远不止本文所讲解的这样简单。本文只是起到一个抛砖引玉的功能。希望读者能够灵活恰当地使用事务功能,而不要过度使用事务,否则可能会对性能起到消极的作用。  

 

 

 

  

 

posted @ 2018-03-19 17:25  PanPan003  阅读(510)  评论(0编辑  收藏  举报