如何在多个DbContext类中强制只有一个事务?

时间:2012-01-06 19:21:25

标签: entity-framework-4 transactions repository-pattern dbcontext

背景
从SO的另一个问题我有一个Winforms解决方案(财务)与许多项目(解决方案的固定项目)。 现在,我的一位客户要求我“升级”解决方案,并添加来自另一个Winforms解决方案(HR)的项目/模块。

我真的不想将这些项目作为现有财务解决方案的固定项目。为此,我正在尝试使用MEF创建将加载GUI,业务逻辑和数据层的插件。

问题:
我有一个上下文(DbContext构建用于实现通用存储库模式)和一个外部上下文列表(使用MEF加载 - 这些上下文代表每个插件的上下文,也使用通用存储库模式)。

假设我有这个:

public class MainContext : DbContext
{
   public List<IPluginContext> ExternalContexts { get; set; }

   // other stuff here
}

public class PluginContext_A : DbContext, IPluginContext
{ /* Items from this context */ }

public class PluginContext_B : DbContext, IPluginContext
{ /* Items from this context */ }

并且在已经加载的MainContext类中,我有两个外部上下文(来自插件)。

考虑到这一点,假设我的事务会影响两者 MainContext和PluginContext_B。

如何在一个事务中的两个上下文中执行更新/插入/删除(统一工作)?

使用IUnityOfWork我可以为最后一项设置SaveChanges()但据我所知,我必须有一个上下文才能作为单个事务工作。

有一种使用MSDTC(TransactionScope)的方法,但这种方法很糟糕,我根本不会使用它(也因为我需要在客户端和服务器上启用MSDTC而且我一直都有崩溃和泄漏)。

更新
系统正在使用SQL 2008 R2。永远不要吼叫。
如果可以以不能扩展到MSDTC的方式使用TransactionScope,那很好,但我从未实现过。我一直使用TransactionScope进入MSDTC。根据关于SO的另一篇文章,有些情况下TS不会进入MSDTC:check here。但我真的更愿意采用其他方式而不是TransactionScope ......

4 个答案:

答案 0 :(得分:3)

如果您使用多个上下文,每个上下文使用单独的连接,并且您希望在单个事务中将数据保存到这些上下文,则必须使用TransactionScope和分布式事务(MSDTC)。

您的链接问题不是这种情况,因为在该场景中,第一个连接不会修改数据,因此可以在开始修改数据的连接之前将其关闭。在您的情况下,数据在多个连接上同时修改,这需要两阶段提交和MSDTC。

您可以尝试通过在多个上下文之间共享单个连接来解决它,但这可能非常棘手。我不确定以下样本的可靠性,但您可以尝试一下:

using (var connection = new SqlConnection(connnectionString))
{
    var c1 = new Context(connection);
    var c2 = new Context(connection);

    c1.MyEntities.Add(new MyEntity() { Name = "A" });
    c2.MyEntities.Add(new MyEntity() { Name = "B" });

    connection.Open(); 

    using (var scope = new TransactionScope())
    {
        // This is necessary because DbContext doesnt't contain necessary methods
        ObjectContext obj1 = ((IObjectContextAdapter)c1).ObjectContext;
        obj1.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        ObjectContext obj2 = ((IObjectContextAdapter)c2).ObjectContext;
        obj2.SaveChanges(SaveOptions.DetectChangesBeforeSave);

        scope.Complete();

        // Only after successful commit of both save operations we can accept changes
        // otherwise in rollback caused by second context the changes from the first
        // context will be already accepted = lost

        obj1.AcceptAllChanges();
        obj2.AcceptAllChanges();
    }
}

Context构造函数定义为:

public Context(DbConnection connection) : base(connection,false) { }

样本本身对我有用,但它有多个问题:

  • 首次使用上下文必须使用封闭连接。这就是为什么我在打开连接之前添加实体的原因。
  • 我宁愿在交易之外手动打开连接,但可能不需要。
  • 两次保存更改都成功运行,而Transaction.Current已分配空的分布式事务ID,因此它应该仍然是本地的。
  • 保存要复杂得多,您必须使用ObjectContext因为DbContext没有所有必要的方法。
  • 它不必在每个场景中都有效。甚至MSDN声称:
  

连接时可能会向DTC提升交易   在一次交易中关闭并重新开启。因为实体   框架会自动打开和关闭连接   考虑手动打开和关闭连接以避免   交易促销。

DbContext API的问题在于,即使您手动打开它也会关闭并重新打开连接,因此如果API始终正确识别它是否在事务上下文中运行且不关闭连接,则它是一个打开的问题。

答案 1 :(得分:0)

@Ladislav Mrnka 你从一开始就是对的:我必须使用MSDTC。

我在这里尝试了很多东西,包括我提供的示例代码。 我已经用变化的野兔测试了很多次但是它不会起作用。错误深入到EF和DbContext的工作方式,并为此改变我终于找到了自己的ORM工具。事实并非如此。

我也和一位对EF有很多了解的朋友(MVP)进行了交谈。 我们在这里测试了一些其他的东西,但它不会按照我想要的方式工作。我最终会遇到多个孤立的事务(我试图将它们与我的示例代码一起使用)并且使用这种方法我没有任何方法可以自动执行完全回滚,我将不得不创建大量的通用/自定义代码手动回滚更改,这里有另一个问题:如果这种回滚失败(这不是回滚,只是更新)会怎么样?

所以,我们在这里找到的唯一方法是使用MSDTC并构建一些工具来帮助调试/测试是否启用了DTC,如果客户端/服务器防火墙没问题就可以了。

非常感谢。 =)

答案 2 :(得分:0)

那么,这种情况在10月19日之前有变化吗?在整个intertubes中,人们建议使用以下代码,但它不起作用:

    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Open();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Open();

    using (var transaction = new TransactionScope(TransactionScopeOption.Required, new TransactionOptions{IsolationLevel = IsolationLevel.ReadUncommitted, Timeout = TimeSpan.MaxValue}))
{
    _contextA.SaveChanges();
    _contextB.SaveChanges();

    // Commit the transaction
    transaction.Complete();
}

    // Close open connections
    (_contextA as IObjectContextAdapter).ObjectContext.Connection.Close();
    (_contextB as IObjectContextAdapter).ObjectContext.Connection.Close();

这对于跨存储库实现单个工作单元类是一个严重的拖累。有什么新方法吗?

答案 3 :(得分:0)

为避免使用MSDTC(分布式事务):

这会强制您在事务中使用一个连接以及只使用一个事务。否则应抛出异常。

注意:至少需要EF6

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(); 
                } 
            } 
        } 
    } 
} 

来源: http://msdn.microsoft.com/en-us/data/dn456843.aspx#existing