OptimisticConcurrencyException 在某些情况下在实体框架中不起作用

Posted

技术标签:

【中文标题】OptimisticConcurrencyException 在某些情况下在实体框架中不起作用【英文标题】:OptimisticConcurrencyException Does Not Work in Entity Framework In Certain Situations 【发布时间】:2011-05-23 02:26:36 【问题描述】:

更新(2010-12-21):根据我一直在做的测试完全重写了这个问题。此外,这曾经是 POCO 特定的问题,但事实证明我的问题不一定是 POCO 特定的。

我正在使用实体框架,并且我的数据库表中有一个时间戳列,应该用于跟踪乐观并发的更改。我已在实体设计器中将此属性的并发模式设置为“固定”,但结果不一致。下面是几个简化的场景,它们展示了并发检查在一种场景中有效,但在另一种场景中无效。

成功抛出 OptimisticConcurrencyException:

如果我附加一个断开连接的实体,如果存在时间戳冲突,SaveChanges 将抛出 OptimisticConcurrencyException:

    [HttpPost]
    public ActionResult Index(Person person) 
        _context.People.Attach(person);
        var state = _context.ObjectStateManager.GetObjectStateEntry(person);
        state.ChangeState(System.Data.EntityState.Modified);
        _context.SaveChanges();
        return RedirectToAction("Index");
    

不抛出 OptimisticConcurrencyException:

另一方面,如果我从数据库中检索我的实体的新副本并在某些字段上进行部分更新,然后调用 SaveChanges(),那么即使存在时间戳冲突,我也不会得到一个 OptimisticConcurrencyException:

    [HttpPost]
    public ActionResult Index(Person person) 
        var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
        currentPerson.Name = person.Name;

        // currentPerson.VerColm == [0,0,0,0,0,0,15,167]
        // person.VerColm == [0,0,0,0,0,0,15,166]
        currentPerson.VerColm = person.VerColm;

        // in POCO, currentPerson.VerColm == [0,0,0,0,0,0,15,166]
        // in non-POCO, currentPerson.VerColm doesn't change and is still [0,0,0,0,0,0,15,167]
        _context.SaveChanges();
        return RedirectToAction("Index");
    

基于 SQL Profiler,Entity Framework 似乎忽略了新的 VerColm(它是时间戳属性),而是使用最初加载的 VerColm。因此,它永远不会抛出 OptimisticConcurrencyException。


更新:根据 Jan 的要求添加额外信息:

请注意,我还在上述代码中添加了 cmets,以与我在执行此示例时在控制器操作中看到的内容一致。

这是更新前我的数据库中 VerColm 的值:0x0000000000000FA7

以下是执行更新时 SQL Profiler 显示的内容:

exec sp_executesql N'update [dbo].[People]
set [Name] = @0
where (([Id] = @1) and ([VerColm] = @2))
select [VerColm]
from [dbo].[People]
where @@ROWCOUNT > 0 and [Id] = @1',N'@0 nvarchar(50),@1 int,@2 binary(8)',@0=N'hello',@1=1,@2=0x0000000000000FA7

请注意,@2 应该是 0x0000000000000FA6,但它是 0x0000000000000FA7

这是更新后我的数据库中的 VerColm:0x0000000000000FA8


有谁知道我可以如何解决这个问题?当我更新现有实体并且存在时间戳冲突时,我希望 Entity Framework 引发异常。

谢谢

【问题讨论】:

请发布保存的代码。 我无法重现这个。当我尝试保存具有时间戳冲突的加载和修改的实体时,我得到一个 OptimisticConcurrencyException。你确定你有时间戳冲突吗?您能否发布您的分析 SQL 查询? 嗨 Jan,我根据您的要求在上面添加了其他信息。 【参考方案1】:

说明

在第二个代码示例中没有得到预期的 OptimisticConcurrencyException 的原因是 EF 检查并发的方式:

当您通过查询数据库检索实体时,EF 将在查询时将所有带有ConcurrencyMode.Fixed 标记属性的值记住为原始的、未修改的值。

然后您更改一些属性(包括 Fixed 标记的属性)并在您的 DataContext 上调用 SaveChanges()

EF 通过将所有 Fixed 标记的 db 列的当前值与 Fixed 标记属性的原始未修改值进行比较来检查并发更新。 这里的关键点是 EF 将时间戳属性的更新视为 正常 数据属性更新。您看到的行为是设计使然。

解决方案/解决方法

要解决此问题,您有以下选项:

    使用您的第一种方法:不要为您的实体重新查询数据库,而是将重新创建的实体附加到您的上下文中。

    将您的时间戳值伪造为当前 db 值,以便 EF 并发检查使用您提供的值,如下所示(另见 this answer 类似问题):

    var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
    currentPerson.VerColm = person.VerColm; // set timestamp value
    var ose = _context.ObjectStateManager.GetObjectStateEntry(currentPerson);
    ose.AcceptChanges();       // pretend object is unchanged
    currentPerson.Name = person.Name; // assign other data properties
    _context.SaveChanges();
    

    您可以通过将时间戳值与请求的时间戳值进行比较来自行检查并发性:

    var currentPerson = _context.People.Where(x => x.Id == person.Id).First();
    if (currentPerson.VerColm != person.VerColm)
    
        throw new OptimisticConcurrencyException();
    
    currentPerson.Name = person.Name; // assign other data properties
    _context.SaveChanges();
    

【讨论】:

感谢您的详细解释。【参考方案2】:

这是另一种更通用且适合数据层的方法:

// if any timestamps have changed, throw concurrency exception
var changed = this.ChangeTracker.Entries<>()
    .Any(x => !x.CurrentValues.GetValue<byte[]>("Timestamp").SequenceEqual(
        x.OriginalValues.GetValue<byte[]>("Timestamp")));
if (changed) throw new OptimisticConcurrencyException();
this.SaveChanges();

它只是检查 TimeStamp 是否已更改并引发并发异常。

【讨论】:

我试过这个,它似乎工作正常。但是,我想抛出一个与 EF 类似的 DbUpdateConcurrencyException,包括 Entities 集合,但是,该集合没有设置器。我试图用 ILSpy 窥探源代码,但如果不涉足大量代码(我没有时间去做),这并没有揭示任何有用的东西。有谁知道如何做到这一点? 注意:示例代码不适用于已删除的实体。应更新为...Any(x =&gt; x.State == EntityState.Modified &amp;&amp; !x.CurrentValues.GetValue&lt;....,否则如果您尝试发出删除操作,则会引发 InvalidOperationException。 我重新设计了这个想法以适应我的 DAL,效果很好而且很简单。谢谢。【参考方案3】:

如果首先是 EF 代码,则使用类似于以下代码的代码。这将从 db 加载的原始时间戳更改为从 UI 加载的时间戳,并确保出现OptimisticConcurrencyEception

db.Entry(request).OriginalValues["Timestamp"] = TimeStamp;

【讨论】:

【参考方案4】:

我已修改 @JarrettV 解决方案以使用 Entity Framework Core。现在它正在遍历上下文中所有已修改的条目,并在标记为并发令牌的属性中查找任何不匹配。也适用于 TimeStamp (RowVersion):

private void ThrowIfInvalidConcurrencyToken()

    foreach (var entry in _context.ChangeTracker.Entries())
    
        if (entry.State == EntityState.Unchanged) continue;

        foreach (var entryProperty in entry.Properties)
        
            if (!entryProperty.IsModified || !entryProperty.Metadata.IsConcurrencyToken) continue;

            if (entryProperty.OriginalValue != entryProperty.CurrentValue)
                                
                throw new DbUpdateConcurrencyException(
                    $"Entity entry.Metadata.Name has been modified by another process",
                    new List<IUpdateEntry>()
                    
                        entry.GetInfrastructure()
                    );
            
        
    

我们只需要在 EF 上下文中保存更改之前调用此方法:

public async Task SaveChangesAsync(CancellationToken cancellationToken)

    ThrowIfInvalidConcurrencyToken();
    await _context.SaveChangesAsync(cancellationToken);

【讨论】:

以上是关于OptimisticConcurrencyException 在某些情况下在实体框架中不起作用的主要内容,如果未能解决你的问题,请参考以下文章