简体   繁体   English

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

[英]OptimisticConcurrencyException Does Not Work in Entity Framework In Certain Situations

UPDATE (2010-12-21): Completely rewrote this question based on tests that I've been doing. 更新(2010-12-21):根据我一直在做的测试完全重写了这个问题。 Also, this used to be a POCO specific question, but it turns out that my question isn't necessarily POCO specific. 此外,这曾经是一个POCO特定的问题,但事实证明我的问题不一定是POCO特定的。

I'm using Entity Framework and I've got a timestamp column in my database table that should be used to track changes for optimistic concurrency. 我正在使用实体框架,我的数据库表中有一个时间戳列,应该用于跟踪乐观并发的更改。 I've set the concurrency mode for this property in the Entity Designer to "Fixed" and I'm getting inconsistent results. 我已将实体设计器中此属性的并发模式设置为“已修复”,并且我得到的结果不一致。 Here are a couple of simplified scenarios that demonstrate that concurrency checking works in one scenario but not in another. 以下是几个简化的场景,它们演示了并发检查在一个场景中工作但在另一个场景中不起作用。

Successfully throws OptimisticConcurrencyException: 成功抛出OptimisticConcurrencyException:

If I attach a disconnected entity, then SaveChanges will throw an OptimisticConcurrencyException if there is a timestamp conflict: 如果我附加一个断开连接的实体,那么如果存在时间戳冲突,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");
    }

Does not throw OptimisticConcurrencyException: 不抛出OptimisticConcurrencyException:

On the other hand, if I retrieve a new copy of my entity from the database and I do a partial update on some fields, and then call SaveChanges(), then even though there is a timestamp conflict, I don't get an 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");
    }

Based on SQL Profiler, it looks like Entity Framework is ignoring the new VerColm (which is the timestamp property) and instead using the originally loaded VerColm. 基于SQL Profiler,看起来Entity Framework忽略了新的VerColm(这是时间戳属性),而是使用最初加载的VerColm。 Because of this, it will never throw an OptimisticConcurrencyException. 因此,它永远不会抛出OptimisticConcurrencyException。


UPDATE: Adding additional info per Jan's request: 更新:根据Jan的要求添加其他信息:

Note that I also added comments to the above code to coincide with what I see in my controller action while working through this example. 请注意,我还在上面的代码中添加了注释,以便与我在执行此示例时在控制器操作中看到的内容一致。

This is the value of the VerColm in my DataBase prior to the update: 0x0000000000000FA7 这是更新前我的DataBase中VerColm的值:0x0000000000000FA7

Here is what SQL Profiler shows when doing the update: 以下是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

Note that @2 should have been 0x0000000000000FA6, but it's 0x0000000000000FA7 请注意,@ 2应该是0x0000000000000FA6,但它是0x0000000000000FA7

Here is the VerColm in my DataBase after the update: 0x0000000000000FA8 这是更新后我的DataBase中的VerColm:0x0000000000000FA8


Does anyone know how I can work around this problem? 有谁知道如何解决这个问题? I'd like Entity Framework to throw an exception when I update an existing entity and there's a timestamp conflict. 当我更新现有实体并且存在时间戳冲突时,我希望实体框架抛出异常。

Thanks 谢谢

Explanation 说明

The reason why you aren't getting the expected OptimisticConcurrencyException on your second code example is due to the manner EF checks concurrency: 您没有在第二个代码示例上获得预期的OptimisticConcurrencyException的原因是由于EF检查并发的方式:

When you retrieve entities by querying your db, EF remembers the value of all with ConcurrencyMode.Fixed marked properties by the time of querying as the original, unmodified values. 通过查询数据库检索实体时,EF会在查询时将ConcurrencyMode.Fixed标记的属性记录为all的值,作为原始未修改的值。

Then you change some properties (including the Fixed marked ones) and call SaveChanges() on your DataContext. 然后更改一些属性(包括Fixed标记的属性)并在DataContext上调用SaveChanges()

EF checks for concurrent updates by comparing the current values of all Fixed marked db columns with the original, unmodified values of the Fixed marked properties. EF通过将所有已Fixed标记的db列的当前值与Fixed标记属性的原始未修改值进行比较来检查并发更新。 The key point here is that EF treats the update of you timestamp property as a normal data property update. 这里的关键点是EF将时间戳属性的更新视为普通数据属性更新。 The behavior you see is by design. 你看到的行为是设计的。

Solution/Workaround 解决方案/解决方法

To workaround you have the following options: 要解决此问题,您有以下选择:

  1. Use your first approach: Don't requery the db for your entity but Attach the recreated entity to your context. 使用您的第一种方法:不要为您的实体重新查询数据库,而是将重新创建的实体附加到您的上下文。

  2. Fake your timestamp value to be the current db value, so that the EF concurrency check uses your supplied value like shown below (see also this answer on a similar question): 将您的timestamp值伪造为当前db值,以便EF并发检查使用您提供的值,如下所示(另请参阅类似问题的答案 ):

     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(); 
  3. You can check for concurrency yourself by comparing your timestamp value to the requeried timestamp value: 您可以通过将时间戳值与已获取时间戳值进行比较来自行检查并发性:

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

Here is another approach that is a bit more generic and fits in the data layer: 这是另一种更通用的方法,适合数据层:

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

It just checks to see if the TimeStamp has changed and throws concurrency exception. 它只是检查TimeStamp是否已更改并引发并发异常。

If it's EF Code first, then use code similar to below code. 如果它首先是EF代码,那么使用类似于下面代码的代码。 This will change the original TimeStamp loaded from db to the one from UI and will ensure OptimisticConcurrencyEception occurs. 这将把从db加载的原始TimeStamp更改为UI中的一个,并确保发生OptimisticConcurrencyEception

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

I have modified @JarrettV solution to work with Entity Framework Core. 我修改了@JarrettV解决方案以使用Entity Framework Core。 Right now it is iterating through all modified entries in context and looking for any mismatch in property marked as concurrency token. 现在它正在遍历上下文中的所有已修改条目,并查找标记为并发令牌的属性中的任何不匹配。 Works for TimeStamp (RowVersion) as well: 适用于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()
                    });
            }
        }
    }
}

And we need only to invoke this method before we save changes in EF context: 我们只需要在保存EF上下文中的更改之前调用此方法:

public async Task SaveChangesAsync(CancellationToken cancellationToken)
{
    ThrowIfInvalidConcurrencyToken();
    await _context.SaveChangesAsync(cancellationToken);
}

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

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