web-dev-qa-db-ja.com

複数のdbcontextsを持つ1つのトランザクション

変更をロールバックするために、ユニットテストでトランザクションを使用しています。単体テストはdbcontextを使用し、テストしているサービスは独自のサービスを使用します。どちらも1つのトランザクションでラップされ、1つのdbcontextがもう1つのトランザクション内にあります。問題は、内側の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!");
        }
    }
}

したがって、基本的に私はこの問題の解決策を持っています(この質問の執筆中に考えました)が、dbcontextがネストされている場合、トランザクションでコミットされていないデータにアクセスできないのはなぜですか? using(dbcontext)がトランザクション自体のようなものである可能性がありますか?その場合は、内部のdbcontextで.SaveChanges()を呼び出しているため、問題はまだ理解できません。

19
Mark Homans

最初のシナリオでは、DbContextsをネストしています。それらのそれぞれについて、データベースへの接続が開かれます。 usingブロック内でサービスメソッドを呼び出すと、TransactionScope内で新しい接続が開かれ、別の接続がすでに開いています。これにより、トランザクションが 分散トランザクション に昇格し、部分的にコミットされたデータ(のDbContext.SaveChanges呼び出しの結果)サービス)外部接続から利用できません。また、分散トランザクションははるかに低速であるため、パフォーマンスが低下するという副作用があることにも注意してください。

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

Enlist=falseパラメータを接続文字列に追加してみてください。これにより、分散トランザクションでの自動参加が無効になり、最初のシナリオで例外が発生します。 2番目のシナリオは、SQL Server 2008以降を使用している場合、トランザクションが昇格されないため、問題なく機能し続けます。 ( SQL Serverの以前のバージョンでも、このシナリオではトランザクションが昇格されます。

また、非常によく似た質問に役立つ この素晴らしい答え も役立つかもしれません。

21
jnovo

更新:この答えは不明確だったようです。それはnotであり、DbContextsをできるだけ長く存続させることを提案しています。むしろ、作業単位パターン/アイデアを使用します。 UOWごとに1つのコンテキスト。通常、これは、HTTPリクエスト、GUIインタラクション、またはテストメソッドごとに1つのコンテキストを意味します。ただし、必要に応じて別の方法で行うことができます。


新しいコンテキストを頻繁に使用することは、アンチパターンです。 1つのコンテキストを作成して渡します。依存関係注入フレームワークを使用して、受け渡しを行うのは非常に簡単です。

常に新しいコンテキストがないのはなぜですか?エンティティオブジェクトのインスタンスを共有して渡すことができるようにするためです。その後、他のコードでそれらを変更し、最後にSaveChangesを呼び出してすべてをアトミックに永続化できます。これは非常に素晴らしいコードにつながります。

ただし、サービスを呼び出す前に外側のdbcontextを閉じて、アサート用の新しいdbcontextを開くと、すべてが正常に機能します

いいえ、これは偶然でした。2番目のコンテキストが接続プールから1番目のコンテキストを再利用したためです。これは保証されておらず、負荷がかかると壊れます。

only分散トランザクションを回避する方法は、開いたままにされている1つの接続を使用することです。

ただし、同じ接続を共有する複数のコンテキストを持つことができます。これを行うには、手動で作成した接続でインスタンス化します。

2
usr

これは機能します:

パブリッククラス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();
                    }
                  }
                }
              }
          }
        }
      }
  }
}

アプリで多くのDbContextを使用する場合に便利です。たとえば、数千のテーブルがある場合-私はモジュールごとに約100のテーブルを持ついわゆる「モジュール」を作成しました。そして、各「モジュール」には単一のコンテキストがある場合がありますが、単一のトランザクションでモジュール間のデータ変更を行う必要があります