Одна транзакция с несколькими dbcontexts
Я использую транзакции в своих модульных тестах для отката изменений. unit test использует 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 для assert, все работает нормально:
[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) похоже на транзакцию? Если это так, я все еще не понимаю проблему, так как я вызываю .SaveChanges() во внутреннем dbcontext.
Ответы
Ответ 1
В первом сценарии вы вложен DbContexts
. Для каждой из них открывается соединение с базой данных. Когда вы вызываете свой метод обслуживания в блоке using
, новое соединение открывается в TransactionScope
, а еще один уже открыт. Это приведет к тому, что ваша транзакция будет повышена до распределенная транзакция и частично зафиксированные данные (результат DbContext.SaveChanges
вызов в службе), не доступный из вашего внешнего соединения. Также обратите внимание, что распределенные транзакции намного медленнее и, следовательно, это имеет побочный эффект снижения производительности.
Во втором сценарии, когда вы открываете и закрываете три подключения, в одной транзакции одновременно открываются только одно соединение. Поскольку эти соединения используют ту же строку соединения, транзакция не будет автоматически продвигаться к распределенному соединению, и поэтому каждое последующее соединение внутри транзакции имеет доступ к изменениям, выполненным предыдущим соединением. p >
Вы можете попробовать добавить параметр Enlist=false
в строку подключения. Это приведет к отключению автоматического включения в распределенную транзакцию, в результате чего исключение будет поднято в вашем первом сценарии. Второй сценарий будет работать безупречно, если вы используете SQL Server 2008 и более поздние версии, поскольку транзакция не будет повышена. (Предыдущие версии SQL Server все равно будут способствовать транзакции в этом сценарии.)
Вы также можете найти полезный этот отличный ответ на довольно похожий вопрос.
Ответ 2
Использование свежих контекстов слишком часто - это анти-шаблон. Создайте один контекст и передайте его. Очень легко сделать прохождение вокруг, используя инфраструктуру инъекции зависимостей.
Однако, если я закрываю внешний dbcontext перед вызовом службы и открываю новый dbcontext для assert, все работает отлично
Нет, это совпадение, потому что второй контекст повторно использовал соединение 1-го из пула соединений. Это не гарантируется и будет ломаться под нагрузкой.
Единственный способ избежать распределенных транзакций - использовать одно соединение, которое было открыто.
Однако вы можете иметь несколько контекстов, совместно использующих одно и то же соединение. Создайте соединение с созданным вручную соединением.
Ответ 3
Это работает:
открытый класс Test1 { public int Id {get; задавать; } public string Имя {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 в вашем приложении.
Например, если у вас тысячи таблиц - я просто создал так называемые "модули" со ста таблицей на модуль. И каждый "модуль" имеет один контекст
иногда, хотя мне нужно делать кросс-модульные изменения данных в одной транзакции
Ответ 4
Внешний контекст кэширует объект, полученный во время вашего размещения.