В некоторых ситуациях OptimisticConcurrencyException не работает в Entity Framework - PullRequest
21 голосов
/ 09 декабря 2010

ОБНОВЛЕНИЕ (2010-12-21): полностью переписал этот вопрос на основе тестов, которые я проводил. Кроме того, раньше это был специфичный для POCO вопрос, но оказалось, что мой вопрос не обязательно специфичен для POCO.

Я использую Entity Framework, и в моей таблице базы данных есть столбец отметки времени, который следует использовать для отслеживания изменений для оптимистичного параллелизма. Я установил режим параллелизма для этого свойства в Entity Designer на «Fixed», и я получаю противоречивые результаты. Вот несколько упрощенных сценариев, которые демонстрируют, что проверка параллелизма работает в одном сценарии, но не в другом.

Успешно генерирует 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.


ОБНОВЛЕНИЕ: Добавление дополнительной информации по запросу Яна:

Обратите внимание, что я также добавил комментарии к приведенному выше коду, чтобы они совпадали с тем, что я вижу в своем действии контроллера при работе с этим примером.

Это значение 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 выдавал исключение при обновлении существующей сущности и конфликте меток времени.

Спасибо

Ответы [ 4 ]

30 голосов
/ 23 декабря 2010

Пояснение

Причина, по которой вы не получаете ожидаемое OptimisticConcurrencyException в своем втором примере кода, связана с тем, как EF проверяет параллелизм:

Когдавы извлекаете сущности, запрашивая вашу базу данных, EF запоминает значение всех с помеченными ConcurrencyMode.Fixed свойствами к моменту запроса как исходные неизмененные значения.

Затем вы изменяете некоторые свойства (включая помеченные Fixed)из них) и вызовите SaveChanges() в вашем DataContext.

EF проверяет наличие одновременных обновлений, сравнивая текущие значения всех отмеченных столбцов Fixed db с исходными неизмененными значениями отмеченных свойств Fixed.Ключевым моментом здесь является то, что EF рассматривает обновление свойства вашей метки времени как нормальное обновление свойства данных.Поведение, которое вы видите, является заданным.

Решение / Обходное решение

Чтобы обойти это, у вас есть следующие опции:

  1. ИспользованиеВаш первый подход: не запрашивать db для вашей сущности, но присоединить воссозданную сущность к вашему контексту.

  2. Подделайте значение вашей метки времени, чтобы быть текущим значением 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. Вы можете проверить параллелизм самостоятельно, сравнив свою метку временизначение к требуемому значению метки времени:

    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();
    
6 голосов
/ 19 апреля 2011

Вот еще один подход, который немного более универсален и вписывается в слой данных:

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

Он просто проверяет, изменилась ли метка времени , и выдает исключение параллелизма.

3 голосов
/ 08 февраля 2012

Если это код EF, используйте код, аналогичный приведенному ниже. Это изменит исходную метку времени, загруженную из базы данных, на другую из пользовательского интерфейса и обеспечит выполнение OptimisticConcurrencyEception.

db.Entry(request).OriginalValues["Timestamp"] = TimeStamp;
0 голосов
/ 05 октября 2018

Я изменил решение @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);
}
...