[英]Understanding Entity Framework transactions and their behavior
我首先使用实体框架代码,当我使用事务时它的行为非常奇怪。
在下面的代码中,您将找到一个简单的示例。 正如您所看到的 - 即使创建了事务 - 它们也永远不会提交。 尽管没有提交 - 我的第一个事务在大约 50% 的单元测试运行中被保存到数据库中。
有时数据仅在 10% 的运行中保存在 DB 中。 我没有合理的解释,并试图解决它。
代码:
[TestMethod]
public void TestConcurrentTransactions2()
{
int invoiceId = 1560;
var context1 = new ApplicationDbContext();
var transaction1 = context1.Database.BeginTransaction();
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (++invoiceId).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context1.Invoices.Add(invoice1);
context1.SaveChanges();
var context2 = new ApplicationDbContext();
var transaction2 = context2.Database.BeginTransaction();
var invoice2 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (++invoiceId).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context2.Invoices.Add(invoice2);
context2.SaveChanges();
//transaction2.Commit();
}
我正在使用 EF 6.2.0 和 Oracle 管理的数据访问提供程序。
这种行为似乎是非常情景化的,重现它的唯一方法是一遍又一遍地重新运行测试用例。 如果我在前一个测试尚未完成执行时更改invoiceId
,我似乎能够更频繁地重现该问题。 在这种情况下 - 测试仍将成功完成执行,但会出现 DB 中的记录。 尽管在大多数情况下,这并不重要,数据将随机推送到数据库。
所以我的问题是:
更新 23.08.2018
因此,这里的代码几乎肯定会在每次运行时至少重现该问题一次:
[TestMethod]
public void Test1()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
var context1 = new ApplicationDbContext();
var transaction1 = context1.Database.BeginTransaction();
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId + i).ToString(),
ClientId = "2",
ExternalSystemId = "0"
};
context1.Invoices.Add(invoice1);
context1.SaveChanges();
//transaction1.Commit();
});
}
这是一个似乎有效的修复尝试,尽管我不太适合服务代码模式。 我需要能够稍后返回到服务并回滚或提交事务。
[TestMethod]
public void Test2()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
using (var context = new ApplicationDbContext())
{
var transaction = context.Database.BeginTransaction();
var invoice = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId + i).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
context.Invoices.Add(invoice);
context.SaveChanges();
//transaction.Commit();
}
});
}
这是我尝试实现使用 DBContext 的服务。 正如您将在代码中看到的 - 解析器将检查是否存在上下文或事务并处理它们。 似乎工作正常。 但是,如果任何并行进程失败会发生什么? 然后调用析构函数吗? 我可能需要有人来审查代码。
public class DbService
{
ApplicationDbContext _context;
DbContextTransaction _transaction;
public DbService()
{
_context = new ApplicationDbContext();
_transaction = _context.Database.BeginTransaction();
}
public void InsertInvoice(int invoiceId)
{
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
_context.Invoices.Add(invoice1);
_context.SaveChanges();
}
~DbService()
{
if (_transaction != null)
{
_transaction.Rollback();
_transaction.Dispose();
}
if (_context != null)
{
_context.Dispose();
}
}
}
并测试:
[TestMethod]
public void Test3()
{
int invoiceId = 3350;
Parallel.For(0, 30, i =>
{
var srvc = new DbService();
srvc.InsertInvoice(invoiceId + i);
});
}
正如@WynDysel在评论部分所建议的那样- 通过在using
块中放置上下文可以解决该问题。
我仍然不知道问题的实际原因。 看起来合乎逻辑,除非明确说要提交 - 提交。 好吧,我想我现在必须接受这个解决方案。
也许我应该澄清一下我一开始没有使用using
块的原因。 这是因为DbContext
是在服务中使用的。 在一个服务中,在同一个事务的范围内有多个操作正在执行。 到数据库的多个实体。 所以当代码准备好commit
- 一个Commit()
方法被执行,所有所做的更改都会立即推送到数据库。 否则,如果在此过程中出现问题,则所有更改都会回滚。 所以为此我需要一个服务,通常不允许使用using
块设计。
长话短说 - 我将使用以下服务来管理context
和transaction
。
public class DbService : IDisposable
{
private bool _isDisposed = false;
private ApplicationDbContext _context;
private DbContextTransaction _transaction;
public DbService()
{
_context = new ApplicationDbContext();
_transaction = _context.Database.BeginTransaction();
}
public void InsertInvoice(int invoiceId)
{
try
{
var invoice1 = new OracleDatabaseService.Models.Invoice()
{
InvoiceId = (invoiceId).ToString(),
ClientId = "3",
ExternalSystemId = "0"
};
_context.Invoices.Add(invoice1);
_context.SaveChanges();
}
catch (Exception)
{
Dispose(false);
throw;
}
}
public void Commit(bool isFinal)
{
if (!_isDisposed)
{
_transaction.Commit();
if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}
public void Rollback(bool isFinal)
{
if (!_isDisposed)
{
if (isFinal)
{
Dispose(false);
}
else
{
_transaction.Rollback();
_transaction.Dispose();
_transaction = _context.Database.BeginTransaction();
}
}
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!_isDisposed)
{
if (disposing)
{
// Free other state (managed objects).
}
if (_transaction != null)
{
if (_transaction.UnderlyingTransaction.Connection != null)
{
_transaction.Rollback();
}
_transaction.Dispose();
}
if (_context != null)
{
_context.Dispose();
}
_isDisposed = true;
}
}
~DbService()
{
Dispose(false);
}
}
仍然可以在using
块中使用该服务。 如果在此过程中出现问题,则应调用析构函数以回滚事务并处理上下文。 有 2 种辅助方法可用于手动提交和回滚更改。 它可以是不再需要服务时的最终提交,也可以是当前事务的临时提交和新事务的初始化,同时保持服务的完整性。
InsertInvoice
方法的上下文也包含在 try/catch 块中,以防出现意外错误。
我不能在生产环境中插入任何pending
事务数据,所以我正在采取所有可能的预防措施! 也许我会在Github
上问一个关于实体框架创建者自己这个问题的问题。
更新 #1
非常不幸,但是我上面提供的代码并不能保证不会插入记录。 使用该服务时,您必须进行一些额外的验证。
例如,这个测试用例有时会导致数据插入数据库:
[TestMethod]
public void TestFail()
{
int invoiceId = 3700;
Parallel.For(0, 30, i =>
{
var srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);
if (i > 15)
{
throw new Exception();
}
});
}
以下代码将保证正确处理上下文:
[TestMethod]
public void TestGood()
{
int invoiceId = 3700;
Parallel.For(0, 30, i =>
{
DbService srvc = null;
try
{
srvc = new DbService();
srvc.InsertInvoice(invoiceId + i, i);
if (i > 25)
throw new Exception();
}
catch(Exception ex)
{
if (srvc != null)
srvc.Dispose();
throw ex;
}
});
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.