繁体   English   中英

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

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

所以我的问题是:

  • 这是自动提交事务的正常行为吗?
  • 如何强制 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);
    });
}

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

我仍然不知道问题的实际原因。 看起来合乎逻辑,除非明确说要提交 - 提交。 好吧,我想我现在必须接受这个解决方案。

也许我应该澄清一下我一开始没有使用using块的原因。 这是因为DbContext是在服务中使用的。 在一个服务中,在同一个事务的范围内有多个操作正在执行。 到数据库的多个实体。 所以当代码准备好commit - 一个Commit()方法被执行,所有所做的更改都会立即推送到数据库。 否则,如果在此过程中出现问题,则所有更改都会回滚。 所以为此我需要一个服务,通常不允许使用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;
        }
    });
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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