我在单元测试中使用事务来回滚更改。单元测试使用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()。
答案 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)
外部上下文缓存您在安排期间检索到的实体。