【问题标题】:EF6 Transaction rollback & concurrency issueEF6 事务回滚和并发问题
【发布时间】:2016-02-09 22:17:36
【问题描述】:

一旦我回滚我的事务并重试,它就会失败并出现 DbUpdateConcurrencyException

查看 SQL 分析器后,我看到第一次 INSERT 查询按预期发送,而第二次尝试更新,即使第一次回滚?!

第一个查询:

exec sp_executesql N'INSERT [dbo].[FiscalReceipt]([PurchaseTime], [ReceiptNumber], [Cash], [Card], [Bank])
VALUES (@0, @1, @2, @3, @4)
SELECT [FiscalReceiptID]
FROM [dbo].[FiscalReceipt]
WHERE @@ROWCOUNT > 0 AND [FiscalReceiptID] = scope_identity()',N'@0 datetime2(7),@1 int,@2 decimal(18,2),@3 decimal(18,2),@4 decimal(18,2)',@0='2016-02-08 15:05:43.9145089',@1=666,@2=1.70,@3=0,@4=0

第二次查询:

exec sp_executesql N'UPDATE [dbo].[FiscalReceipt]
SET [PurchaseTime] = @0, [Cash] = @1
WHERE ([FiscalReceiptID] = @2)
',N'@0 datetime2(7),@1 decimal(18,2),@2 int',@0='2016-02-08 15:12:11.8101261',@1=555.00,@2=2042

附:提交某事时,SQL Profiler 表的 EventClass 列中不应该有 TM:Rollback 或 TM:Commit 吗?

C#代码:

注意: OutOfPaperException 旨在被忽略,并且有意提交该范围内的事务。任何其他异常都应回滚更改。

var transaction = DatabaseContext.Database.BeginTransaction();

            try
            {
                // Commented for debugging
                //IFiscalPrinter fPrinter = DeviceManager.GetFiscalPrinter();
                //var lastReceiptNumber = fPrinter.GetLastReceiptNumber();

                // false data for debugging
                var lastReceiptNumber = 666;
                Receipt.ReceiptNumber = lastReceiptNumber++;
                Receipt.PurchaseTime = DateTime.Now;

                DatabaseContext.SaveChanges();

                //fPrinter.PrintFiscalReceipt(Receipt);

                // Exception for debugging purpose
                if (Receipt.Cash < 500)
                {
                    throw new Exception();
                }

                transaction.Commit();

                Close();
            }
            catch (OutOfPaperException)
            {
                if (transaction != null)
                {
                    transaction.Commit();
                    Close();
                }

                MessageBoxService.ShowMessage("Promenite papir pre sledećeg štampanja!", "Nema više papira!", MessageButton.OK, MessageIcon.Warning);
            }
            catch (Exception ex)
            {
                // I added this but this doesn't help
                Receipt.PurchaseTime = new DateTime();
                Receipt.ReceiptNumber = 0;
                //Receipt.FiscalReceiptID = 0; <- I'm not allowed to do this

                if (transaction != null)
                {
                    transaction.Rollback();
                }

                MessageBoxService.ShowMessage(ex.Message, "Greška!", MessageButton.OK, MessageIcon.Error);
            }
            finally
            {
                if (transaction != null)
                {
                    transaction.Dispose();
                }
            }

DatabaseContext 的生命周期等于 ViewModel 的生命周期。

编辑: 将适当的条目状态更改为 EntryState.Added 会导致操作成功,但感觉很脏。事务回滚/失败时条目不应该保持在已添加状态吗?

EDIT2:运行此代码后:

using (MetalShopDB ctx = new MetalShopDB())
using (var transaction = ctx.Database.BeginTransaction())
{
    var receipt = new FiscalReceipt()
    {
        ReceiptNumber = 555,
        PurchaseTime = DateTime.Now,
        Cash = 100
    };

    ctx.FiscalReceipts.Add(receipt);

    Console.WriteLine("Has changes " + ctx.ChangeTracker.HasChanges());
    Console.WriteLine(ctx.Entry(receipt).State);

    ctx.SaveChanges();

    Console.WriteLine("Saved changes");

    Console.WriteLine("Has changes " + ctx.ChangeTracker.HasChanges());
    Console.WriteLine(ctx.Entry(receipt).State);

    transaction.Rollback();

    Console.WriteLine("Rolled back");

    Console.WriteLine("Has changes " + ctx.ChangeTracker.HasChanges());
    Console.WriteLine(ctx.Entry(receipt).State);
}

我得到了这个输出,我觉得这很奇怪,因为人们会期望上下文与数据库同步,所以当你回滚时,上下文应该跟进更改。

Has changes True
Added
Saved changes
Has changes False
Unchanged
Rolled back
Has changes False
Unchanged

【问题讨论】:

    标签: c# entity-framework transactions


    【解决方案1】:

    为您的 EDIT2

    当调用 SaveChanges 方法时,如果保存时没有发现错误(这是因为您回滚之后的情况),则会调用 ObjectContext.AcceptAllChanges() 方法,该方法接受更改并填充主键,外键和更改条目状态。

    回滚只回滚事务,而不是在对象上下文/更改跟踪器上。

    此时再次调用 SaveChanges 为时已晚,因为即使您进行了回滚,实体也已经填充了数据库信息。

    原始问题

    在 SaveChanges 成功完成

    之后,您会抛出一个错误(用于调试)
    if (Receipt.Cash < 500)
    {
        throw new Exception();
    }
    

    所以按照前面的逻辑,AcceptAllChanges 已经被调用了。

    编辑

    您可以通过使用 ObjectContext 保存来控制 AcceptAllChanges

    var objectContext = ((IObjectContextAdapter) ctx).ObjectContext;
    objectContext.SaveChanges(SaveOptions.DetectChangesBeforeSave);
    
    transaction.Commmit();
    objectContext.AcceptAllChanges();
    

    【讨论】:

    • 谢谢,乔纳森。这是否是在 EF6 中处理事务以便 Context 反映回滚的正确方法?
    • 是的,也不是,人们通常不会遇到此类事务问题,因为他们在 SaveChanges 发生后提交。在您的情况下,您抛出一个与 SaveChanges 操作没有任何关系的错误,这就是问题所在。如果可以,您应该在“SaveChanges”之前进行验证,但您的情况可能不太可能。
    • 问题是我不能打印收据,除非它也被保存了。这似乎是一种合乎逻辑的方式,因为 DB 事务是可逆的,而打印则不是。
    • 所以我相信使用对象上下文中的 SaveChanges 并控制 AcceptAllChanges 是要走的路 ;)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-10-04
    • 1970-01-01
    • 1970-01-01
    • 2015-06-30
    • 2012-05-04
    相关资源
    最近更新 更多