簡體   English   中英

一個具有多個 dbcontexts 的事務

[英]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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM