![](/img/trans.png)
[英]Multiple databases (multiple DbContexts) in one transaction using TransactionScope
[英]One transaction with multiple dbcontexts
我在單元測試中使用事務來回滾更改。 單元測試使用 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 時無法訪問事務中未提交的數據。 可能是因為 using(dbcontext) 就像一個事務本身? 如果是這樣,我仍然不明白這個問題,因為我在內部 dbcontext 上調用 .SaveChanges()。
在第一個場景中,您正在嵌套DbContexts
。 為每個數據庫打開一個連接。 當您在using
塊中調用服務方法時,會在TransactionScope
打開一個新連接,而另一個連接已打開。 這會導致您的事務被提升為 分布式事務,並且部分提交的數據(服務中的DbContext.SaveChanges
調用的結果)無法從您的外部連接獲得。 另請注意,分布式事務要慢得多,因此會降低性能。
在第二種情況下,當您打開和關閉三個連接時,在您的事務中只有一個連接同時打開。 由於這些連接共享相同的連接字符串,事務不會自動提升為分布式連接,因此,事務中的每個后續連接都可以訪問由前一個連接執行的更改。
您可以嘗試將Enlist=false
參數添加到您的連接字符串中。 這將禁用分布式事務中的自動登記,導致在您的第一個場景中引發異常。 如果您使用的是 SQL Server 2008 及更高版本,第二種情況將繼續完美運行,因為事務不會得到提升。 ( 在這種情況下,SQL Server 的先前版本仍將促進事務。 )
更新:這個答案似乎不清楚。 這並不是建議讓 DbContexts 盡可能長時間地存活。 相反,使用工作單元模式/想法。 每個 UOW 一個上下文。 通常,這意味着每個 HTTP 請求、每個 GUI 交互或每個測試方法都有一個上下文。 但如果需要,可以采用不同的方式。
過於頻繁地使用新鮮的上下文是一種反模式。 創建一個上下文並將其傳遞。 使用依賴注入框架很容易進行傳遞。
為什么不是一直都有新的上下文? 因為您希望能夠共享實體對象實例並傳遞它們。 然后其他代碼可以修改它們,最后您調用SaveChanges
以原子方式保留所有內容。 這導致了非常好的代碼。
但是,如果我在調用服務之前關閉外部 dbcontext 並為斷言打開一個新的 dbcontext,則一切正常
不,這是一個巧合,因為第二個上下文重用了連接池中第一個的連接。 這是不能保證的,並且會在負載下損壞。
避免分布式事務的唯一方法是使用一個一直保持打開的連接。
但是,您可以讓多個上下文共享相同的連接。 使用手動創建的連接進行實例化以執行此操作。
這有效:
public class Test1 { public int Id { 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();
}
}
}
}
}
}
}
}
}
在您的應用程序中使用許多 DbContext 時,它很有用。 例如,如果您有數千個表 - 我剛剛創建了所謂的“模塊”,每個模塊大約有一百個表。 雖然我需要在單個事務中進行跨模塊數據修改,但有時每個“模塊”都有單個上下文
對我來說,在配置文件中使用 '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;
}
}
外部上下文正在緩存您在安排期間檢索到的實體。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.