Найти в Дзене
Архитектура на .NET

Разрешение конфликтов конкурентных запросов в EF Core

При обновлении свойств сущности EF Core по умолчанию собирает все изменения в одну транзакцию, которая при выполнении dbContext.SaveChanges() либо применяется целиком, либо полностью откатывается в случае ошибки.
Обычно паттерн работы с сущностями выглядит так: достали из базы, обновили свойства, сохранили результат.
Для примера представим, что наша сущность - это заказ, у которого есть статус: Тогда обработчик изменения статуса заказа будет выглядеть примерно так: При выполнении последней строки в БД пойдёт запрос, обновляющий свойство Status у сущности, примерно такой: Проблема Что будет, если два пользователя системы одновременно вызовут этот метод для одного и того же заказа, но указав разный целевой статус? Оба запроса будут обрабатываться в разных потоках, оба достанут order со статусом New, потом обновят значение статуса на желаемое (в одном потоке на Canceled, а в другом на Done), оба сохранят свои изменения, и завершатся успехом.
Но какой статус в итоге будет у нашего зак

При обновлении свойств сущности EF Core по умолчанию собирает все изменения в одну транзакцию, которая при выполнении dbContext.SaveChanges() либо применяется целиком, либо полностью откатывается в случае ошибки.

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

Для примера представим, что наша сущность - это заказ, у которого есть статус:

Тогда обработчик изменения статуса заказа будет выглядеть примерно так:

-2

При выполнении последней строки в БД пойдёт запрос, обновляющий свойство Status у сущности, примерно такой:

-3

Проблема

Что будет, если два пользователя системы одновременно вызовут этот метод для одного и того же заказа, но указав разный целевой статус? Оба запроса будут обрабатываться в разных потоках, оба достанут order со статусом New, потом обновят значение статуса на желаемое (в одном потоке на Canceled, а в другом на Done), оба сохранят свои изменения, и завершатся успехом.

Но какой статус в итоге будет у нашего заказа? Неопределённый: либо New, либо Canceled, в зависимости от того, SQL-запрос из какого потока выполнился быстрее.

Желаемым поведением системы, однако, было бы успешное выполнение только одного из запросов, и возвращение ошибки при выполнении другого, т.к. за время его выполнения обрабатываемая сущность по факту уже изменилась, и надо явно сообщить об этом пользователю. Как это можно сделать?

Ввести понятие
версии сущности, получаеть его из БД при загрузке сущности, и проверять его при сохранении. Если при сохранении выяснилось, что в БД у сущности уже другая версия, значит что-то пошло не так.

EF Core предлагает несколько способов управления конфликтами конкурентных запросов при сохранении данных. Часть из них задействуют механизмы СУБД, часть полностью полагается на уровень приложения.

Вот эти способы:

  1. Если вы используете SQL Server, в сущность можно добавить поле `Version` таким образом:
-4

2. Если вы не хотите полагаться на специфику СУБД:

-5

3. Если вы используете PostgreSQL, вам доступен наиболее лаконичный вариант, не требующий "замусоривания" бизнес-сущностей sql-специфичными полями, т.к. всё решается при конфигрурировании сущности:

-6

Дело в том, что PostgreSQL автоматически ведёт учёт транзакций, обновляя соответствующий счётчик для каждой сущности. Значение этого счётчика находится в "скрытой" колонке, которая уже есть в любой таблице. Эта колонка называется xmin.

Сгенерированный в этом случае SQL-запрос будет выглядеть так:

-7

Обработать ошибку сохранения во всех случаях можно одинаково:

-8