简体   繁体   English

了解实体框架事务及其行为

[英]Understanding Entity Framework transactions and their behavior

I am using Entity Framework code-first and it's behaving very strangely when I use transactions.我首先使用实体​​框架代码,当我使用事务时它的行为非常奇怪。

In the code below you will find a simple example.在下面的代码中,您将找到一个简单的示例。 As you can see - even though transactions are created - they are never committed.正如您所看到的 - 即使创建了事务 - 它们也永远不会提交。 Despite NOT having a commit - my first transaction is being saved to the database in about 50% of unit test runs.尽管没有提交 - 我的第一个事务在大约 50% 的单元测试运行中被保存到数据库中。

Sometimes data only saved in DB in 10% of runs.有时数据仅在 10% 的运行中保存在 DB 中。 I have no reasonable explanation and loosing my mind trying to solve it..我没有合理的解释,并试图解决它。

Code:代码:

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

I'm using EF 6.2.0 and an Oracle-managed data access provider.我正在使用 EF 6.2.0 和 Oracle 管理的数据访问提供程序。

This behavior seems to be very situational and the only way to reproduce it is by re-running the test case over and over again.这种行为似乎是非常情景化的,重现它的唯一方法是一遍又一遍地重新运行测试用例。 I seem to be able to repro the issue more often if I go and change invoiceId while the previous test hasn't finished executing.如果我在前一个测试尚未完成执行时更改invoiceId ,我似乎能够更频繁地重现该问题。 In this case - the test will still finish executing successfully but a record in DB will appear.在这种情况下 - 测试仍将成功完成执行,但会出现 DB 中的记录。 Though in most cases, this doesn't really matter and data will be pushed to DB randomly.尽管在大多数情况下,这并不重要,数据将随机推送到数据库。

So my questions are:所以我的问题是:

  • Is this an OK behavior that transaction is being committed automatically?这是自动提交事务的正常行为吗?
  • How do I force EF to only commit transactions when I need to?如何强制 EF 仅在需要时提交事务?
  • Is there an alternative mechanism to handle transactions in EF?是否有其他机制来处理 EF 中的事务?

UPDATE 23.08.2018更新 23.08.2018

So, here is a code which almost certainly will repro the issue at least once per run:因此,这里的代码几乎肯定会在每次运行时至少重现该问题一次:

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

Here is a fix attempt that seems to be working, though I don't fit into a service code pattern very well.这是一个似乎有效的修复尝试,尽管我不太适合服务代码模式。 I need to be able to come back to a service later and either roll-back or commit a transaction.我需要能够稍后返回到服务并回滚或提交事务。

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

This is my attempt to implement a service that uses DBContext.这是我尝试实现使用 DBContext 的服务。 As you will see in the code - a descructor will check whether there is a context or transaction present and dispose of them.正如您将在代码中看到的 - 解析器将检查是否存在上下文或事务并处理它们。 Seems to be working OK.似乎工作正常。 Though, what will happen if any of the parallel processes fail?但是,如果任何并行进程失败会发生什么? Is the descructor called then?然后调用析构函数吗? I need somebody to review the code, possibly.我可能需要有人来审查代码。

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

and test:并测试:

[TestMethod]
public void Test3()
{
    int invoiceId = 3350;

    Parallel.For(0, 30, i =>
    {
        var srvc = new DbService();
        srvc.InsertInvoice(invoiceId + i);
    });
}

As was suggested by @WynDysel in a comment section - the problem is resolvable by putting a context in a using block.正如@WynDysel在评论部分所建议的那样- 通过在using块中放置上下文可以解决该问题。

The actual reasons for the issue are still unknown to me.我仍然不知道问题的实际原因。 It looks logical, that unless something is explicitly said to be committed - to be committed.看起来合乎逻辑,除非明确说要提交 - 提交。 Well, I guess I have to live with this solution for now.好吧,我想我现在必须接受这个解决方案。

Perhaps I should make some clarifications about the reasons why I was not using the using block to begin with.也许我应该澄清一下我一开始没有使用using块的原因。 It's because the DbContext is used from within a service.这是因为DbContext是在服务中使用的。 Within a service there are multiple operations being done in scope of the same transaction.在一个服务中,在同一个事务的范围内有多个操作正在执行。 To multiple entities of database.到数据库的多个实体。 So when the code is ready for commit - a Commit() method is executed and all of the changes done are pushed to DB at once.所以当代码准备好commit - 一个Commit()方法被执行,所有所做的更改都会立即推送到数据库。 Otherwise if something goes wrong along the way, then all of the changes are rolled back.否则,如果在此过程中出现问题,则所有更改都会回滚。 So for this I needed a service and normally am not allowed to use a using block by design.所以为此我需要一个服务,通常不允许使用using块设计。

To make a long story short - I will be using following service for managing context and transaction .长话短说 - 我将使用以下服务来管理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);
    }
}

It is still possible to use the service in a using block.仍然可以在using块中使用该服务。 If something along the way goes wrong then a destructor shall be called to roll back the transaction and dispose of a context.如果在此过程中出现问题,则应调用析构函数以回滚事务并处理上下文。 There are 2 helper methods for committing and rolling back chnages manually.有 2 种辅助方法可用于手动提交和回滚更改。 It could either be a final commit when the service is no longer needed or a temporary commit of current transaction and initialization of a new transaction while keeping an integrity of a service.它可以是不再需要服务时的最终提交,也可以是当前事务的临时提交和新事务的初始化,同时保持服务的完整性。

InsertInvoice method's contexts are also wrapped in a try/catch block in case something unexpected goes wrong. InsertInvoice方法的上下文也包含在 try/catch 块中,以防出现意外错误。

I can't afford to insert any pending transaction data on a production environment so am taking all possible precautions!我不能在生产环境中插入任何pending事务数据,所以我正在采取所有可能的预防措施! Perhaps I will be asking a question on Github about this issue Entity Framework creators themselves.也许我会在Github上问一个关于实体框架创建者自己这个问题的问题。

Update #1更新 #1

It is very unfortunate, but the code I provided above does NOT guarantee that records will not be inserted.非常不幸,但是我上面提供的代码并不能保证不会插入记录。 You have to make some additional validations, when using the service.使用该服务时,您必须进行一些额外的验证。

For example, this testcase will cause the data to be inserted into database sometimes:例如,这个测试用例有时会导致数据插入数据库:

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

And following code will guarantee disposal of context correctly:以下代码将保证正确处理上下文:

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

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM