2014-01-18 14 views
11

私は単体テストでトランザクションを使用して変更をロールバックしています。単体テストはdbcontextを使い、私がテストしているサービスは自分自身を使用します。両方とも1つのトランザクションでラップされ、1つのdbcontextは他のトランザクションのブロック内にあります。問題は、内側のdbcontextが変更を保存するときに、外側のdbcontextには表示されません(他のdbcontextにすでにオブジェクトがロードされている可能性があるためです)。次に例を示します。複数のdbcontextを持つ1つのトランザクション

[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!"); 
     } 
    } 
} 

サービス:私はサービスを呼び出す前に、外dbcontextを閉じて、アサートのための新しいdbcontextを開くと

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

     } 
    } 
} 

しかし、すべてが正常に動作します:

[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コールの結果)が外部接続から利用できなくなります。また、分散トランザクションははるかに遅いため、パフォーマンスが低下するという副作用があります。

2つ目のシナリオでは、3つの接続を開いたり閉じたりしている間に、トランザクション内で同時に1つの接続のみが開いています。これらの接続はの同じ接続文字列を共有するため、トランザクションは自動的に分散接続に昇格されないため、トランザクション内の後続の各接続は前の接続によって行われた変更にアクセスできます。

接続文字列にEnlist=falseパラメータを追加してみてください。これにより、分散トランザクションの自動登録が無効になり、最初のシナリオで例外が発生します。 2番目のシナリオは、トランザクションが促進されないため、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

この問題は見つけるのは難しいですが、それに関する「公式の」Microsoftのブログがあり、それを実証しました。私は手元にリンクがありません。 – usr

2

新しいコンテキストを頻繁に使用することは、アンチパターンです。 1つのコンテキストを作成して渡します。依存性注入フレームワークを使用して渡すことは非常に簡単です。私はサービスを呼び出す前に、外dbcontextを閉じて、アサートのための新しいdbcontextを開くと

しかし、すべてが第二コンテキストがの接続を再利用するのでいいえ、これは偶然の一致だった

正常に動作します接続プールから1番目。これは保証されておらず、負荷がかかると破損します。

分散トランザクションを回避するために唯一方法が開いたままにされてきた一つの接続を使用することです。

同じ接続を複数のコンテキストで共有することはできますが、手動で作成した接続でインスタンス化してください。

+3

私は確かに、インターネット上でも、msdnでも全く反対のことを読んでいると確信しています。 – sam

+1

@sam特に具体的に言及しているものはありますか?残りの部分は疑いのない事実なのでおそらく最初のパラグラフ。コンテキストは、あなたの作業単位が行く限り生きることになっています。これは通常HTTPリクエストです。これは議論の余地のある意見ではない。より短いライフタイムも可能ですが、アプリ全体でエンティティを共有できません。エンティティは1つのコンテキストでしか存続できません。 – usr

+2

dbcontextを可能な限り長くすることについて、これが初めてのことです。ウェブ技術に関する1つの要求と、クライアントアプリケーションでのより短い寿命は、いつも得意なものです。私たちが長生き/短命の定義に依存するかもしれません。 – sam

0

これは動作します:

パブリッククラスTest1を { 公共のint Idを{取得します。セット; } 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を使用する場合に便利です。 たとえば、何千ものテーブルがある場合、モジュールごとに約100個のテーブルを持ついわゆる「モジュール」を作成しました。また、各モジュールは単一のコンテキスト を持つことがありますが、1つのトランザクションでモジュール間のデータ変更を行う必要があります。

-1

外部コンテキストは、アレンジ中に取得されたエンティティをキャッシュしています。

+0

回答が短すぎる - コメントにする必要があります。 –

+0

@KarlNicholasコメントは短い答えではありません。コメントは質問を明確にするためのもので、答えは質問への回答の投稿用です。長さは、何がコメントか回答でなければならないかを判断するのには関係ありません。重要な情報が不足しているため、その質問に対する回答が役に立たないと思われる場合は、回答に投票することでその意見を反映させることはできますが、回答ではありません。 – Servy

+0

さあ、確かに@Servy –

関連する問題