2014-01-18 56 views
11

我在單元測試中使用事務來回滾更改。單元測試使用了一個dbcontext,並且我正在測試的服務使用他自己的。它們都被封裝在一個事務中,並且一個dbcontext在另一個事務的塊中。問題是,當內部dbcontext保存其更改時,它對外部dbcontext不可見(並且我不認爲這是因爲其他dbcontext可能已經加載了該對象)。這裏是例子:一個事務與多個dbcontexts

[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()。

回答

14

在第一種情況下,您嵌套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到一個非常類似的問題。

+0

即使使用相同的連接字符串,您也可以擁有分佈式事務。它們將比跨服務器連接更輕量級,但仍然是分佈式的。這種影響是非確定性的,因此非常危險。在測試過程中,你通常很幸運。 OP是幸運的。所以雖然這個答案被接受並且被高度投票,但在這個特殊方面仍然是不正確的。 – usr

+0

@usr你能提供一個我可以查看的鏈接,以改善答案嗎?隨意編輯它自己 – jnovo

+0

這個問題很難找到,但有關於它的「官方」微軟博客,我已經展示了它。我手邊沒有鏈接。 – usr

2

過於頻繁地使用新鮮的上下文是一種反模式。創建一個上下文並傳遞它。使用依賴注入框架來傳遞很容易。

但是,如果我調用服務之前關閉外的DbContext並打開一個新的DbContext的斷言,一切工作正常

不,這是一個巧合,因爲第二個方面重複使用的連接第一個來自連接池。這是不能保證的,並會在負載下崩潰。

避免分佈式事務的唯一方法是使用已經保持打開一個連接。

不過,您可以有多個上下文共享相同的連接。使用手動創建的連接來實例化。

+3

我很確定我讀了所有在互聯網上和msdn上的確切相反 – sam

+1

@sam你特別指的是什麼?可能是第一段,因爲其餘的都是不爭的事實。只要你的工作單元完成,環境就會生存下去。這通常是一個HTTP請求。這不是一個有爭議的觀點。更短的生命週期也是可能的,但它們阻止你在整個應用程序中共享實體。一個實體只能生活在一個環境中。 – usr

+2

說真的,這是我第一次閱讀關於使dbcontext儘可能長壽命。每個Web技術請求一個,客戶端應用程序的生命更短,這是我總是得到的。這可能取決於我們對長/短命的定義。 – sam

0

這工作:

公共類的Test1 { 公衆詮釋標識{獲得;組; } 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(); 
        } 
        } 
       } 
       } 
      } 
     } 
     } 
    } 
} 

它在你的應用程序使用許多DbContexts時非常有用。 例如,如果您有數千個表 - 我只是創建了所謂的「模塊」,每個模塊大約有一百個表。並且每個「模塊」有單個上下文 有時雖然我需要在單個事務中執行跨模塊數據修改

-1

外部上下文緩存在您的排列期間檢索到的實體。

+0

太短的答案 - 應該是一個評論。 –

+0

@KarlNicholas評論是不是簡短的答案。評論是要求澄清問題,答案是發佈問題的答案。長度與確定什麼應該是評論或答案無關。如果您認爲對這個問題的回答沒有用,因爲它缺乏重要的信息,您可以通過對答案進行投票來反映這一意見,但這並不表示答案。 – Servy

+0

是的,肯定的事情@Servy –

相關問題