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

Редактирование и версия карточки

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

В Item с начала серии есть поле Version. До этого оно сохранялось, но не участвовало в решении о записи. Теперь GET редактирования читает текущую версию, а форма отправляет её обратно как скрытое поле. SQL обновляет строку только при совпадении ID и версии и одновременно увеличивает счётчик. Это оптимистический контроль: читатель не удерживает блокировку всё время заполнения формы, а проверяет основание изменения в момент сохранения.

Открытая форма является предложением

Маршрут GET /items/{id}/edit получает ID из пути и читает конкретную строку. Некорректный идентификатор или отсутствующая карточка приводят к отсутствию ресурса, а ошибка SQL — к внутренней ошибке чтения. В форме виден артикул, но он отмечен readonly: текущая операция меняет название, место и количество, сохраняя SKU. Это выбранная граница предметной операции, а не защита на основании HTML-атрибута.

В POST сервер не использует присланный SKU для обновления. Поэтому изменение readonly через инструменты браузера не подменяет артикул в базе. Не нужно доверять тому, что поле нельзя было нажать обычным способом. Внутренний ID берётся из маршрута, а Version проходит преобразование и проверку положительности. Если поле версии отсутствует, нельзя молча считать его единицей: сервер потеряет смысл проверки исходного состояния.

result,err:=a.db.ExecContext(r.Context(),"UPDATE items SET name=?,location=?,quantity=?,version=version+1 WHERE id=? AND version=?",p.Name,p.Location,p.Quantity,id,version)

Приведённый SQL находится внутри update в полном main.go состояния lesson-10. Название и место подставляются параметрами, а версия участвует в WHERE. Увеличение выполнено в той же команде. Если сначала прочитать версию, сравнить её в Go и затем выполнить безусловный UPDATE, между этими действиями появится окно другой записи. Проверка на стороне базы связывает сравнение и изменение в одну атомарную операцию.

Количество изменённых строк

Успешное выполнение SQL не означает, что строка действительно обновилась. Условие WHERE может не найти совпадение. Поэтому сервер получает RowsAffected. В нашем договоре одна изменённая строка означает принятое предложение, а ноль — отсутствие подходящего состояния. Это важный вид результата, который нельзя заменить проверкой только err == nil. База могла корректно выполнить команду и правильно отказаться применять её к устаревшей версии.

Рассмотрим карточку клавиатуры версии 1. В первой вкладке пользователь меняет место на «переговорная», во второй — название на «Механическая клавиатура». Первая отправка обновляет строку и делает версию 2. Вторая всё ещё содержит основание версии 1. Её условный UPDATE не находит строку, поэтому сервер возвращает 409 и сохраняет введённое предложение в форме. Новое название не потеряно из интерфейса, но и не выдано за записанное состояние.

Ответ предлагает открыть свежую карточку и перенести изменения. Мы не подменяем скрытую Version свежим числом автоматически. Иначе повторное нажатие молча подтвердит перезапись полей, которые человек ещё не видел. Для более удобного интерфейса можно показать сравнение старых и новых значений и позволить объединить независимые поля. Но это отдельная модель разрешения конфликта, а не просто повтор SQL с новым счётчиком.

Граница одного счётчика

Версия относится ко всей карточке. Изменение любого из поддерживаемых полей делает старую форму конфликтующей, даже если два человека редактировали разные поля. Такой подход иногда отказывает чаще, чем необходимо, но понятен и предотвращает тихую потерю обновления. Контроль отдельных полей потребовал бы сохранять исходные значения либо версии каждого поля. Для небольшого учёта важнее ясное поведение, чем максимально широкое автоматическое объединение.

Ноль изменённых строк в текущем обработчике также может означать, что карточка больше не существует. Мы не добавили удаление, поэтому в обычном маршруте курса вероятнее устаревшая версия, однако прямое изменение базы может дать другой случай. Более точный интерфейс мог бы прочитать наличие строки и различить причины после отказа. Такое дополнительное чтение помогает сообщению, но не должно снова превращаться в отдельную предварительную проверку перед безусловной записью.

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

Ошибка в обычной странице и htmx

409 с HTML-формой работает и как полная страница, и как panel для интерактивного запроса. Обработчик beforeSwap из предыдущего урока разрешает замену этого представления. Но он не принимает изменение сам: ошибка сохраняется в статусе, SQL не записывает предложенные поля, версия базы остаётся актуальной. Такое разделение даёт человеку возможность исправить конфликт без ложного сообщения об успехе.

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

Дополнительный ошибочный вариант — оставить WHERE с версией, но забыть version=version+1. Первая запись тогда не делает другие формы устаревшими, и механизм теряет свою цель. Ещё один вариант — увеличить версию отдельным SQL после UPDATE. Между командами состояние может оказаться неполным. Согласованность требует, чтобы проверка основания и новый счётчик выполнялись вместе с предметным изменением.

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

Исходники урока

Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-10. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.

Первичные источники: документация 1.