一个具有多个dbcontexts的事务

时间:2014-01-18 10:40:37

标签: c# .net dbcontext transactionscope

我在单元测试中使用事务来回滚更改。单元测试使用dbcontext,我正在测试的服务使用自己的。它们都包装在一个事务中,一个dbcontext在另一个事务的块中。问题是,当内部dbcontext保存他的更改时,外部dbcontext不可见(我不认为这是因为另一个dbcontext可能已经加载了对象)。这是一个例子:

[TestMethod]
public void EditDepartmentTest()
{
    using (TransactionScope transaction = new TransactionScope())
    {
        using (MyDbContext db = new MyDbContext())
        {
            //Arrange
            int departmentId = (from d in db.Departments
                                   where d.Name == "Dep1"
                                   select d.Id).Single();
            string newName = "newName",
                   newCode = "newCode";

            //Act
            IDepartmentService service = new DepartmentService();
            service.EditDepartment(departmentId, newName, newCode);

            //Assert
            Department department = db.Departments.Find(departmentId);
            Assert.AreEqual(newName, department.Name,"Unexpected department name!");
            //Exception is thrown because department.Name is "Dep1" instead of "newName"
            Assert.AreEqual(newCode, department.Code, "Unexpected department code!");
        }
    }
}

服务:

public class DepartmentService : IDepartmentService
{
    public void EditDepartment(int DepartmentId, string Name, string Code)
    {
        using (MyDbContext db = new MyDbContext ())
        {
            Department department = db.Departments.Find(DepartmentId);

            department.Name = Name;
            department.Code = Code;

            db.SaveChanges();

        }
    }
}

但是,如果我在调用服务之前关闭外部dbcontext并为断言打开一个新的dbcontext,那么一切正常:

[TestMethod]
public void EditDepartmentTest()
{
    using (TransactionScope transaction = new TransactionScope())
    {
        int departmentId=0;
        string newName = "newName",
               newCode = "newCode";

        using (MyDbContext db = new MyDbContext())
        {
            //Arrange
            departmentId = (from d in db.Departments
                                   where d.Name == "Dep1"
                                   select d.Id).Single();
        }

        //Act
        IDepartmentService service = new DepartmentService();
        service.EditDepartment(departmentId, newName, newCode);

        using (MyDbContext db = new MyDbContext())
        {
            //Assert
            Department department = db.Departments.Find(departmentId);
            Assert.AreEqual(newName, department.Name,"Unexpected department name!");
            Assert.AreEqual(newCode, department.Code, "Unexpected department code!");
        }
    }
}

所以基本上我有一个解决这个问题的方法(在写这个问题的过程中想到它)但我仍然想知道为什么在嵌套dbcontexts时不可能访问事务中的未提交数据。 可能是因为使用(dbcontext)就像一个事务本身?如果是这样,我仍然不明白这个问题,因为我在内部dbcontext上调用.SaveChanges()。

5 个答案:

答案 0 :(得分:20)

在第一个场景中,您正在嵌套DbContexts。将为每个数据库打开与数据库的连接。当您在using块中调用服务方法时,会在TransactionScope内打开一个新连接,而另一个连接已打开。这会导致您的事务被提升为distributed transaction,并且您的外部连接无法获得部分提交的数据(服务中DbContext.SaveChanges调用的结果)。另请注意,分布式事务要慢得多,因此会产生降低性能的副作用。

在第二种情况下,当您打开和关闭三个连接时,只有一个连接在您的事务中同时打开。由于这些连接共享相同的连接字符串,因此事务不会自动提升为分布式连接,因此事务中的每个后续连接都可以访问上一个连接所执行的更改。

您可以尝试将Enlist=false参数添加到连接字符串中。这将禁用分布式事务中的自动登记,从而导致在第一个场景中引发异常。如果您使用SQL Server 2008及更高版本,第二种方案将保持完美运行,因为事务不会得到提升。 (Prior versions of SQL Server will still promote the transaction in this scenario.

您也可以在一个非常相似的问题上找到有用的this great answer

答案 1 :(得分:2)

更新:似乎这个答案还不清楚。它建议尽可能长时间保持DbContexts活着。相反,使用工作单元模式/想法。每个UOW一个上下文。通常,这意味着每个HTTP请求,每个GUI交互或每个测试方法有一个上下文。但如果需要,可以采用不同的方式。


过于频繁地使用新环境是一种反模式。创建一个上下文并传递它。使用依赖注入框架进行传递非常容易。

为什么不一直没有新的背景?因为您希望能够共享实体对象实例并传递它们。然后其他代码可以修改它们,最后您调用SaveChanges以原子方式保留所有内容。这导致非常好的代码。

  

但是,如果我在调用服务之前关闭外部dbcontext并为断言打开一个新的dbcontext,那么一切正常

不,这是巧合,因为第二个上下文重用了连接池中第一个的连接。这不能得到保证,并会在负载下中断。

避免分布式事务的唯一方法是使用一个保持打开的连接。

但是,您可以让多个上下文共享相同的连接。使用手动创建的连接进行实例化。

答案 2 :(得分:1)

对我来说,在配置文件中使用'Enlist = false'消除了DST错误,但也使事务无效,即使未达到作用域scope.Complete(),数据库更改仍是持久的:

using (var scope = new TransactionScope())  
using (var firstContext = new db1Context()) 
using (var secondContext = new db2Context())
{
// do smth with dbContexts
 scope.Complete();
}

我最终使用DbContextTransaction解决了这个问题:

using (var firstContext = new db1Context())
using (var secondContext = new db2Context())
using (DbContextTransaction firstContextTransaction = db1Context.Database.BeginTransaction())
using (DbContextTransaction secondContextTransaction = db2Context.Database.BeginTransaction())
    {
    try
    {
    // do smth with dbContexts                    
    firstContextTransaction.Commit();
    secondContextTransaction.Commit();
    }
    catch (System.Exception)
    {
    firstContextTransaction?.Rollback();
    secondContextTransaction?.Rollback();
    throw;
    }
    }

答案 3 :(得分:0)

这有效:

公共类Test1     {         public int Id {get;组; }         public string Name {get;组; }     }

public class Test2
{
    public int Id { get; set; }
    public string Name { get; set; }
}

public class DC1 : DbContext
{
    public DbSet<Test1> Test1 { get; set; }

    public DC1(SqlConnection conn)
        : base(conn, contextOwnsConnection: false)
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.HasDefaultSchema("dc1");

        modelBuilder.Entity<Test1>().ToTable("Test1");
    }
}

public class DC2 : DbContext
{
    public DbSet<Test2> Test2 { get; set; }

    public DC2(SqlConnection conn)
        : base(conn, contextOwnsConnection: false)
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        modelBuilder.HasDefaultSchema("dc2");

        modelBuilder.Entity<Test2>().ToTable("Test2");
    }
}

...

using (var conn = new SqlConnection(ConfigurationManager.ConnectionStrings["EntityConnectionString"].ConnectionString))
{
    conn.Open();

    using (var tr = conn.BeginTransaction())
    {
        try
        {
            using (var dc1 = new DC1(conn))
            {
                dc1.Database.UseTransaction(tr);
                var t = dc1.Test1.ToList();
                dc1.Test1.Add(new Test1
                {
                    Name = "77777",
                });
                dc1.SaveChanges();
            }
            //throw new Exception();
            using (var dc2 = new DC2(conn))
            {
                dc2.Database.UseTransaction(tr);
                var t = dc2.Test2.ToList();
                dc2.Test2.Add(new Test2
                {
                    Name = "777777",
                });
                dc2.SaveChanges();
            }
            tr.Commit();
        }
        catch
        {
            tr.Rollback();
            //throw;
        }
        App.Current.Shutdown();
    }
}

我想最好在事务外部获取,因此不会发生锁定,但我不确定 - 需要自己进行调查

更新: 上面的代码使用代码优先的方法 以下代码适用于数据库优先

public MetadataWorkspace GetWorkspace(Assembly assembly)
{
    MetadataWorkspace result = null;
    //if (!mCache.TryGetValue(assembly, out result) || result == null)
    {
        result = new MetadataWorkspace(
            new string[] { "res://*/" },
            new Assembly[] { assembly });
        //mCache.TryAdd(assembly, result);
    }
    return result;
}

...

using(var conn = new SqlConnection("..."))
{
  conn.Open();
  using(var tr = conn.BeginTransaction())
  {
        using(var entityConnection1 = new EntityConnection(
            GetWorkspace(typeof(DbContext1).Assembly), conn))
      {
        using(var context1 = new ObjectContext(entityConnection1))
        {
          using(var dbc1 = new DbContext1(context1, false))
          {
            using(var entityConnection2 = new EntityConnection(
                GetWorkspace(typeof(DbContext2).Assembly), conn))
            {
                using(var context2 = new ObjectContext(entityConnection2))
                {
                  using(var dbc2 = new DbContext2(context2, false))
                  {
                    try
                    {
                        dbc1.UseTransaction(tr);
                        // fetch and modify data
                        dbc1.SaveChanges();

                        dbc2.UseTransaction(tr);
                        // fetch and modify data
                        dbc2.SaveChanges();

                        tr.Commit();
                    }
                    catch
                    {
                        tr.Rollback();
                    }
                  }
                }
              }
          }
        }
      }
  }
}

在app中使用多个DbContexts时非常有用。 例如,如果你有数千个表 - 我刚刚创建了所谓的&#34;模块&#34;每个模块大约有一百个表。每个&#34;模块&#34;有单一的背景 有时虽然我需要在单个事务中进行跨模块数据修改

答案 4 :(得分:-1)

外部上下文缓存您在安排期间检索到的实体。