了解实体框架交易及其行为

时间:2018-08-22 14:45:35

标签: c# entity-framework-6

我先使用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中的记录。尽管在大多数情况下,这并不重要,但数据将被随机推送到数据库。

所以我的问题是:

  • 这是自动执行事务的正常行为吗?
  • 如何强制EF仅在需要时才提交事务?
  • 是否有其他机制可以处理EF中的交易?

更新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);
    });
}

1 个答案:

答案 0 :(得分:1)

正如 @WynDysel 在评论部分中所建议的-通过将上下文放在using块中可以解决此问题。

该问题的实际原因仍然未知。看起来很合逻辑,除非明确声明要提交某些内容,否则必须提交。好吧,我想我现在必须使用此解决方案。

也许我应该澄清一下为什么我没有使用using块的原因。这是因为DbContext是在服务中使用的。在一个服务中,在同一事务的范围内要执行多个操作。 到数据库的多个实体。因此,当代码为commit准备就绪时-将执行Commit()方法,并将所有所做的更改立即推送到DB。否则,如果在此过程中出现问题,则会回滚所有更改。因此,为此我需要一项服务,通常在设计上不允许使用using块。

长话短说-我将使用以下服务来管理contexttransaction

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;
        }
    });
}