Атомное приращение с инфраструктурой сущностей

У меня есть сервер MySQL, к которому я обращаюсь, используя Entity Framework 4.0. В базе данных у меня есть таблица с именем Работы, в которую входят некоторые подсчеты. Я разрабатываю веб-сайт с Asp.net. В этой таблице одновременно появляется еще один пользователь. И эта ситуация вызывает неправильную проблему изъятия.

Мой код:

dbEntities myEntity = new dbEntities();

var currentWork = myEntity.works.Where(xXx => xXx.RID == 208).FirstOrDefault();
Console.WriteLine("Access work");

if (currentWork != null)
{
    Console.WriteLine("Access is not null");
    currentWork.WordCount += 5;//Default WordCount is 0
    Console.WriteLine("Count changed");
    myEntity.SaveChanges();
    Console.WriteLine("Save changes");
}
Console.WriteLine("Current Count:" + currentWork.WordCount);

Если один поток, кроме потока, обращается к базе данных одновременно, остаются только последние изменения.

Токовый выход:

t1: Thread One - t2: Thread Two

t1: Работа с доступом

t2: Работа с доступом

t2: доступ не равен null

t1: Доступ не равен null

t1: смененный счетчик

t2: количество измененных

t1: сохранить изменения

t2: сохранить изменения

t1: Текущее количество: 5

t2: Текущее количество: 5

Ожидаемый результат:

t1: Работа с доступом

t2: Работа с доступом

t2: доступ не равен null

t1: Доступ не равен null

t1: смененный счетчик

t2: количество измененных

t1: сохранить изменения

t2: сохранить изменения

t1: Текущее количество: 5

t2: Текущее количество: 10

Я знаю, почему эта проблема возникает, потому что этот код не является атомарным. Как я могу запустить атомную операцию?

Ответ 1

В Entity Framework вы не можете сделать это "атомной" операцией. У вас есть следующие шаги:

  • Загрузить объект из базы данных
  • Сменить счетчик в памяти
  • Сохранить измененный объект в базе данных

В промежутке между этими шагами другой клиент может загрузить объект из базы данных, которая все еще имеет старое значение.

Лучший способ справиться с этой ситуацией - использовать оптимистичный concurrency. В основном это означает, что изменение на шаге 3 не будет сохранено, если счетчик больше не тот, что был, когда вы загрузили объект на шаге 1. Вместо этого вы получите исключение, которое вы можете обработать, перезагрузив объект и повторное внесение изменений.

Рабочий процесс будет выглядеть следующим образом:

  • В объекте Work свойство WordCount должно быть помечено как токен concurrency (аннотации или Fluent API в случае Code-First)
  • Загрузить объект из базы данных
  • Сменить счетчик в памяти
  • Вызвать SaveChanges в блоке try-catch и перехватить исключения типа DbUpdateConcurrencyException
  • Если возникает исключение, перезагрузите объект в блоке catch из базы данных, снова примените это изменение и снова вызовите SaveChanges
  • Повторите последний шаг, пока не произойдет больше исключений

В этом ответе вы можете найти пример кода для этой процедуры (используя DbContext).

Ответ 2

Следующий способ будет работать, если вы размещаете свой сайт в одном процессе (он не будет работать с веб-фермой или веб-gardsen):

   private static readonly Locker = new object();

   void Foo()
   {
          lock(Locker)
          {
                 dbEntities myEntity = new dbEntities();

                 var currentWork = myEntity.works.Where(xXx => xXx.RID == 208).FirstOrDefault();
                 Console.WriteLine("Access work");

                 if (currentWork != null)
                 {
                     Console.WriteLine("Access is not null");
                     currentWork.WordCount += 5;//Default WordCount is 0
                     Console.WriteLine("Count changed");
                     myEntity.SaveChanges();
                     Console.WriteLine("Save changes");
                 }
                 Console.WriteLine("Current Count:" + currentWork.WordCount);
          }
   }

Что еще вы можете сделать, это использовать необработанный SQL-запрос через ObjectContext:

   if (currentWork != null)
             {
                 Console.WriteLine("Access is not null");
                 myEntity.ExecuteStoredCommand("UPDATE works SET WordCount = WordCount + 5 WHERE RID = @rid", new MySqlParameter("@rid", MySqlDbType.Int32){Value = 208)
                 Console.WriteLine("Count changed");

             }
    var record = myEntity.works.FirstOrDefault(xXx => xXx.RID == 208);
    if(record != null)
        Console.WriteLine("Current Count:" + record .WordCount);