我先使用Entity Framework代码,并且在使用事务时表现很奇怪。
在下面的代码中,您将找到一个简单的示例。如您所见-即使创建了事务-它们也从未提交。尽管没有提交-我的第一个事务在大约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);
});
}
答案 0 :(得分:1)
正如 @WynDysel 在评论部分中所建议的-通过将上下文放在using
块中可以解决此问题。
该问题的实际原因仍然未知。看起来很合逻辑,除非明确声明要提交某些内容,否则必须提交。好吧,我想我现在必须使用此解决方案。
也许我应该澄清一下为什么我没有使用using
块的原因。这是因为DbContext
是在服务中使用的。在一个服务中,在同一事务的范围内要执行多个操作。
到数据库的多个实体。因此,当代码为commit
准备就绪时-将执行Commit()
方法,并将所有所做的更改立即推送到DB。否则,如果在此过程中出现问题,则会回滚所有更改。因此,为此我需要一项服务,通常在设计上不允许使用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;
}
});
}