Управление параллельным доступом

27

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

По умолчанию в Entity Framework используется модель оптимистического параллелизма. Оптимистичность означает то, что она основана на предположении, что никто другой не модифицирует данные, пока они используются, и данные сохраняются в базе без проведения проверки, не были ли они изменены кем-то другим. Это поведение показано в следующем примере:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            Customer cust = context.Customers
                .Where(c => c.CustomerID == "LAZYK")
                .Select(c => c)
                .First();

            Console.WriteLine("Исходное значение {0}", cust.ContactName);

            // Изменить запись вне Entity Framework
            ExecuteStatementInDb(String.Format(
                    @"update Customers set ContactName = 'Samuel Arthur Sanders' 
                      where CustomerID = 'LAZYK'"));

            // Получить значение из базы данных Entity Framework
            string dbValue = GetStringFromDb(String.Format(
                @"select ContactName from Customers
                where CustomerID = 'LAZYK'"));

            Console.WriteLine("Значение в базе данных: {0}", dbValue);

            // Модифицируем заказчика 
            cust.ContactName = "John Doe";

            // Сохраняем изменения
            context.SaveChanges();

            Console.WriteLine("Конечное значение: {0}", cust.ContactName);

С помощью LINQ to Entities загружается сущностный объект для Customer со значением CustomerID, равным LAZYK. Затем запись обновляется непосредственно, за пределами Entity Framework, и значение ContactName становится Samuel Arthur Sanders. После этого значение читается из базы данных, также за пределами Entity Framework, в результате чего сущностный объект Customer перестает быть синхронизированным с базой данных.

С использованием сущностного объекта Customer изменяется значение ContactName на John Doe и вызывается метод SaveChanges. Entity Framework записывает внесенное изменение в базу данных и, поскольку оптимистическая стратегия означает надежду, что никто другой не изменял данные, переписывает изменения, внесенные вне Entity Framework.

Включение проверок параллелизма

Прежде чем записать изменения, можно заставить Entity Framework проверить, не была ли модифицирована база данных кем-то другим. Это также оптимистический параллелизм, поскольку в базе данных ничего не блокируется, пока работа ведется с сущностными объектами, однако при этом исключается проблема, продемонстрированная выше.

Понадобится включить проверку параллельного доступа на уровне поля. Если нужно, чтобы конфликты параллелизма проверялись для всех полей сущностного объекта, следует убедиться, что отредактированы все поля. Нет способа сообщить Entity Framework о необходимости, чтобы каждое изменение сущностного типа или даже каждое изменение всей модели данных проверялось автоматически.

Для решения проблемы, которую можно было видеть в предыдущем примере, понадобится включить проверку параллельного доступа на поле ContactName сущностного типа Customer. Первый шаг — открытие файла .edmx двойным щелчком в окне Solution Explorer и нахождением сущностного типа Customer в представлении визуального конструктора:

Сущностный тип Customer

Щелкните на свойстве ContactName для открытия подробной информации в окне Properties и измените значение Concurency Mode (Режим параллелизма) на Fixed, как показано на рисунке:

Установка режима параллельного доступа для свойства ContactName

Наконец, сохраните изменения, выбрав пункт меню File --> Save NorthwindEntityModel.edmx в Visual Studio.

Обработка конфликтов параллелизма

Как только включить проверку конфликтов параллелизма для поля сущностного объекта, сразу возникает исключение OptimisticCincurrencyException при попытке обновить данные, которые были модифицированы с момента загрузки сущностных объектов. Попробуйте запустить предыдущий код и вместо вывода вы получите исключение. Давайте модифицируем этот пример, включив обработку конфликтов параллельного доступа:

// Создание ObjectContext
            NorthwindEntities context = new NorthwindEntities();

            Customer cust = context.Customers
                .Where(c => c.CustomerID == "LAZYK")
                .Select(c => c)
                .First();

            Console.WriteLine("Исходное значение {0}", cust.ContactName);

            // Изменить запись вне Entity Framework
            ExecuteStatementInDb(String.Format(
                    @"update Customers set ContactName = 'Samuel Arthur Sanders' 
                      where CustomerID = 'LAZYK'"));

            // Модифицируем заказчика 
            cust.ContactName = "Rex Colmers";

            // Сохраняем изменения
            try
            {
                context.SaveChanges();
            }
            catch (OptimisticConcurrencyException)
            {
                Console.WriteLine("Обнаружен конфликт параллельного доступа - разрешить.");
            }
            finally
            {
                string dbValue = GetStringFromDb(String.Format(@"select ContactName from Customers
                where CustomerID = 'LAZYK'"));
                Console.WriteLine("Значение в базе данных: {0}", dbValue);
                Console.WriteLine("Кэшированное значение: {0}", cust.ContactName);
            }

Здесь выполняется та же последовательность запросов, что и в первом примере: получается сущностный объект Customer для записи со значением CustomerID, равным LAZYK, изменяется поле ContactName за пределами Entity Framework, вносятся те же изменения в Entity Framework и затем вызывается метод SaveChanges.

Вызов SaveChanges помещен в блок try...catch...finally. Поскольку включена проверка параллельного доступа в поле ContactName, известно, что при попытке обновления базы данных будет получено исключение OptimisticConcurrencyException. В блоке finally на консоль выводится значение ContactName из базы данных и значение из сущностного объекта. Компиляция и запуск этого кода даст следующий вывод:

Пример проблемы параллельного доступа

В конечном итоге получается база данных с одним значением и кэшированный сущностный объект, имеющий конфликтующее значение тех же данных. Это уже шаг вперед — по крайней мере, запись в базу данных не выполняется без предварительной их проверки. Но теперь необходимо решить проблему различий в значениях данных, чтобы привести их в соответствие и попытаться (дополнительно) заново обновить. Это делается с помощью метода ObjectContext.Refresh. Ниже приведен отрывок модифицированного примера:

...
            try
            {
                context.SaveChanges();
            }
            catch (OptimisticConcurrencyException)
            {
                Console.WriteLine("Обнаружен конфликт параллельного доступа - обновить данные.");
                context.Refresh(RefreshMode.StoreWins, cust);
            }
            finally
            {
               ...

В этом примере при перехвате исключения OptimisticConcurrencyException вызывается метод Refresh. Метод Refresh принимает два аргумента. Первый — значение из перечисления RefreshMode, а второй — объект, который должен быть обновлен. Перечисление RefreshMode включает два значения — StoreWins и ClientWins. Значение StoreWins обновляет указанный объект на основе информации из базы данных. Поэтому в примере можно рассчитывать на то, что и значение в сущностном объекте, и значение в базе данных будет равно Samuel Arthur Adams. Компиляция и запуск кода дают ожидаемый результат:

Использование метода Refresh

Давайте еще раз посмотрим, что здесь происходит. Предпринимается попытка записать обновление в строку базы данных, которая была модифицирована кем-то другим. Entity Framework обнаруживает конфликт параллельного доступа и генерирует исключение OptimisticConcurrencyException, чтобы уведомить о наличии проблемы. Сущностный объект, который был модифицирован, обновляется из базы данных и приводится в согласованное состояние.

Но что происходит с обновлением? Ничего — оно не применяется. Если все-таки необходимо применить изменения, несмотря на то, что кто-то модифицировал те же самые данные, которые используются, то следует применить значение ClientWins из перечисления RefreshMode и вызвать SaveChanges заново:

try
            {
                context.SaveChanges();
            }
            catch (OptimisticConcurrencyException)
            {
                Console.WriteLine("Обнаружен конфликт параллельного доступа - обновить данные.");
                context.Refresh(RefreshMode.ClientWins, cust);
                context.SaveChanges();
            }
            ...

На этот раз указано значение ClientWins, что соответствует утверждению "о конфликте параллелыюго доступа известно, но необходимо сохранить изменения". Снова понадобится вызвать SaveChanges. Вызов метода Refresh просто очищает конфликт параллельного доступа для Entity Framework и не записывает изменений. Если скомпилировать и запустить этот код, получится следующий результат:

Запись обновления после конфликта параллельного доступа

Здесь видно, что изменение, проведенное через Entity Framework, было записано в базу данных.

Пройди тесты
Лучший чат для C# программистов