Перейти к содержанию

Версия проекта и конфликт двух изменений

Анна и Вера открыли один проект «Севера». Анна меняет его название на «Зимний план», а Вера, ещё видя прежнюю форму, сохраняет «План выставки». Если оба запроса просто выполняют UPDATE, последнее сохранение незаметно уничтожит результат первого. Права и принадлежность при этом могут быть совершенно правильными.

Для такого случая в проекте есть version. Клиент получает её вместе с данными и передаёт как ExpectedVersion. Сервер изменяет строку только при совпадении. В этом уроке разберём ожидаемое поведение конфликта и отличие версии от блокировки. Код находится в WorkspaceEndpoints.cs полного примера; запросы при подготовке не выполнялись.

Версия описывает прочитанное состояние

Первый ответ проекта содержит version: 1. Обе формы запоминают это число. Анна отправляет expectedVersion: 1, сервер меняет имя и сохраняет версию 2. Вера затем передаёт прежнее число 1; условие UPDATE уже не соответствует строке, поэтому второй запрос не меняет имя.

Ожидаемый ответ Вере — 409 с кодом version_conflict. Клиент перечитывает проект и предлагает человеку повторить действие на новых данных. Важно, что конфликт не означает запрет роли: Вера всё ещё editor. Он означает, что основание её изменения устарело и человек должен решить, как совместить две правки.

Ниже приведён полный SQL условного переименования. Организация остаётся в условии независимо от версии; это разные проверки. Версия не является уникальным секретом и не может заменить принадлежность.

UPDATE projects
SET name=@name, version=version+1
WHERE organization_id=@organization
  AND id=@id
  AND version=@expected_version
  AND deleted_at IS NULL
RETURNING id,name,version;

При успешном ответе клиент сохраняет новую версию. Если он продолжает отправлять старую после собственного успешного изменения, следующий запрос тоже получает конфликт. Сервер не обязан угадывать, что это тот же пользователь и что «скорее всего» новое изменение безопасно. Основание задаётся явно, поэтому поведение одинаково для браузера, мобильного клиента и скрипта.

Почему чтение перед UPDATE недостаточно

Можно сначала прочитать версию, сравнить её в C#, а затем выполнить безусловный UPDATE. Между этими двумя действиями другая транзакция успеет изменить строку. Проверка в приложении была верной в момент чтения, но не в момент записи. Условие в UPDATE объединяет проверку и изменение в одно действие базы.

Отдельное предварительное чтение уместно для понятного ответа, но оно не должно быть единственным условием успеха. В нашем обработчике SQL возвращает изменённую строку; отсутствие результата запускает ограниченный поиск в текущей компании. Если проект исчез или был мягко удалён, возвращается 404. Если существует с другой версией — 409.

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

Версия хранится как bigint и увеличивается только при предметном изменении. Простое чтение не должно менять её. Если каждый просмотр увеличивал бы число, открытая вкладка постоянно теряла бы основание записи из-за других читателей. Метрики посещения и версия документа относятся к разным данным.

Блокировка нужна для составного решения

Приглашение из предыдущего урока меняет несколько связанных состояний, а смена роли проверяет последнего владельца. Для таких решений мы используем FOR UPDATE на строке, которая определяет порядок операций. Документация PostgreSQL 18 об explicit locking описывает конфликты блокировок строк; наша задача — выбрать предметную строку для согласования.

Нельзя блокировать только случайно выбранного владельца, если правило относится ко всей организации. Два параллельных запроса могут выбрать разных участников и не встретиться. Блокировка организации даёт общую точку. Аналогично при добавлении задачи блокируется её проект, потому что именно состояние проекта определяет допустимость новой дочерней записи.

Порядок блокировок должен быть одинаковым: организация, затем членство, затем прикладной объект. Если один маршрут сначала берёт проект, а другой сначала организацию, встречные операции могут образовать deadlock. PostgreSQL обнаружит такую ситуацию и откатит одну транзакцию, но предсказуемый порядок снижает вероятность отказа и упрощает объяснение.

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

Повтор запроса после потери ответа

Конфликт версии защищает от потери чужой правки, но не делает POST автоматически идемпотентным. Если создание проекта завершилось, а ответ потерялся, повторное создание с тем же именем создаст второй проект. Это другая проблема. В биллинге введём ключ команды, потому что повтор финансовой операции должен возвращать тот же заказ.

Для переименования повтор с той же версией после успешного commit получит 409. Клиент может перечитать строку и сравнить желаемое имя с фактическим. Это понятное поведение учебного API. Не нужно автоматически возвращать успех только по совпавшему имени: между запросами могли произойти другие существенные изменения, которые простое сравнение имени не отражает.

В ручном сценарии сначала сохраните Анной версию 1, затем отправьте Верой другую правку с той же версией. Ожидается одно сохранённое имя и один конфликт. После перечитывания Вера использует версию 2 и может выполнить согласованную правку. В аудите будут два завершённых изменения, а отказ не будет изображён как третье.

Версия не превращается в часы

Значение version не является timestamp. Его нельзя сравнивать с текущим временем или использовать как дату последнего изменения. Оно выражает последовательность состояний одной строки. У двух проектов обе версии могут равняться пяти, не имея никакой общей истории. Для отображения момента изменения добавьте отдельное поле времени и не подменяйте им условие атомарного UPDATE. При восстановлении мягко удалённого проекта версия также должна увеличиваться, чтобы старая форма не продолжила запись как будто удаления не было.

Таким образом, компания определяет область, роль — разрешение, а версия — актуальность основания изменения. Эти проверки независимы и должны оставаться видны. Следующий урок добавит Row-Level Security, чтобы забытое условие области в SQL не становилось единственной причиной утечки данных.

Оглавление серии