Границы транзакции
Ранее отдельная вставка или обновление меняла одну строку. Редакторская операция часто затрагивает несколько объектов: дописать отсутствующий урок, затем открыть курс читателям. Если эти команды фиксируются независимо, после сбоя база может сохранить только часть задуманного действия. Транзакция объединяет выбранные изменения в одну границу фиксации.
Продолжим на PostgreSQL 17.11, сохранив прежние шесть курсов и двенадцать учебных уроков. Самостоятельный снимок catalog-db-advanced/lesson-17 находится в архиве продолжения. SQL и программы не запускались; ниже описаны ожидаемые эффекты. reset.sql отдельно и разрушительно восстанавливает учебную схему, а пример изменения заканчивается откатом.
Предметная операция из двух команд
Курс web-performance с ключом четыре пока черновик. У его второй главы поле body отсутствует. В нашем исходном договоре статус курса не означает завершённость всех глав, однако редактор хочет выполнить конкретное действие: заполнить эту заготовку и опубликовать карточку одновременно. Такое намерение шире отдельного UPDATE.
BEGIN;
UPDATE catalog.lessons
SET body = 'Текст о полевых данных'
WHERE course_id = 4 AND position = 2;
UPDATE catalog.courses
SET status = 'published',
published_at = TIMESTAMPTZ '2025-02-05 10:00+00',
version = version + 1
WHERE id = 4 AND status = 'draft'
RETURNING id, status, published_at, version;
Внутри этой транзакции ожидается заполненный текст и опубликованный курс с версией два. Момент выбран как фиксированное учебное значение, а не получен реальными часами сервера. Статус и время меняются одной командой, поскольку существующее ограничение требует их соответствия после изменения строки. Сам BEGIN не разрешает временно нарушать любую немедленную проверку.
Связь курса с главой важна и для условия обновления. Если второй урок не найден, первая команда затронет ноль строк. SQL не обязан считать отсутствие совпадения ошибкой. Для полного предметного договора приложение проверяет число изменённых объектов или возвращённые значения и решает, можно ли продолжать. Учебный набор гарантирует строку, но реальный клиент должен учитывать удаление и другой исходный курс.
Граница охватывает именно записи в PostgreSQL. Если после первой команды отправить внешнее письмо, откат базы не заберёт письмо у получателя. Поэтому публикация во внешние системы требует отдельного согласования с фиксацией. Нельзя считать BEGIN общим выключателем для всех действий программы, которые случайно произошли рядом с SQL.
Свои изменения и чужое наблюдение
Клиент, выполняющий транзакцию, видит собственные изменения. Другой клиент не должен прочитать её незавершённое промежуточное состояние как обычные подтверждённые строки. Общее понятие атомарной операции раскрыто в руководстве транзакций PostgreSQL. В нашем случае другой читатель не получает комбинацию «опубликованная карточка, но ещё не заполненная глава» вследствие промежуточной фиксации этих двух команд.
Чтобы увидеть собственный результат до решения о сохранении, в снимке находится чтение:
SELECT c.slug, c.status, l.position, l.body
FROM catalog.courses AS c
JOIN catalog.lessons AS l ON l.course_id = c.id
WHERE c.id = 4 AND l.position = 2;
ROLLBACK;
До отката ожидается новое состояние. После отдельного повторного SELECT ожидаются прежний draft и отсутствующий body. Операция специально не заканчивается COMMIT, поэтому лабораторный пример не оставляет опубликованный курс после завершения. Однако во время открытой транзакции он всё равно является настоящим изменением и может удерживать ресурсы.
Если вместо ROLLBACK приложение выбрало успешный COMMIT, связанные изменения становятся сохранённым результатом. Но клиент должен узнать, что фиксация завершилась успешно, прежде чем сообщать пользователю окончательное «Опубликовано». Полученный ранее RETURNING показывает изменённую строку, а не заменяет решение о завершении всей операции. Это различие уже было заметно в уроке создания курса.
Ошибка и состояние блока
После SQL-ошибки текущая транзакция PostgreSQL обычно оказывается в прерванном состоянии. Следующий обычный SELECT не исправляет её автоматически. Клиент должен откатить блок либо вернуться к заранее созданной точке сохранения в подходящем сценарии. Пытаться бесконечно повторять тот же UPDATE в прерванном блоке означает не устранить причину, а снова встретить состояние ошибки.
В основном lesson.sql намеренной ошибки нет. Она объясняется текстом, чтобы клиент с остановкой при первой ошибке не оставался незаметно посреди неподготовленного сценария. Представьте несовместимый статус без времени публикации: ограничение отклонит изменение. Правильная реакция всей операции зависит от решения редактора, но сохранить только первую команду автоматически было бы нарушением выбранной границы.
BEGIN не создаёт вложенную независимую транзакцию, если блок уже открыт. Для частичного отката используются savepoints. Правила начала блока описаны в справочнике BEGIN. Поэтому библиотека, которая уже управляет транзакцией, должна быть согласована с вызывающим кодом: два слоя не могут независимо обещать одну и ту же фиксацию без общего договора.
Точка сохранения
В отдельном savepoint-demo.sql рассматривается необязательное редактирование описания после заполнения главы. Точка позволяет отменить этот дополнительный фрагмент, сохранив более раннее изменение внутри ещё открытого блока:
BEGIN;
UPDATE catalog.lessons SET body = 'Текст о полевых данных'
WHERE course_id = 4 AND position = 2;
SAVEPOINT optional_description;
UPDATE catalog.courses SET description = 'Временная подпись' WHERE id = 4;
ROLLBACK TO SAVEPOINT optional_description;
SELECT description FROM catalog.courses WHERE id = 4;
ROLLBACK;
После возврата к точке описание снова отсутствует. Заполненная глава внутри блока ещё видна своему клиенту, но финальный полный откат отменяет и её. Возврат к savepoint не является подтверждением изменений для остальных соединений. Он только меняет состояние текущей незавершённой операции.
Нужно ли сохранять часть операции после ошибки? Для публикации обязательные шаги должны остаться единым результатом. Savepoint полезен только для действительно необязательной части с понятной политикой отказа. Если обернуть каждый шаг в отдельную точку и бездумно продолжать, можно получить логически неполное действие даже при технически успешном COMMIT.
Если ответ о фиксации потерялся
Есть ещё один исход, который отличается от обычной SQL-ошибки: соединение оборвалось во время подтверждения, и клиент не получил понятного ответа. Само отсутствие ответа не доказывает откат. База могла сохранить действие, а сообщение о результате не дошло до приложения. Поэтому нельзя автоматически повторить публикацию и считать, что первый раз точно ничего не произошло.
Для нашей карточки можно сначала прочитать актуальные status, published_at и version, затем сопоставить их с ожидаемым действием. Но совпавший статус не всегда устанавливает авторство операции: тот же курс мог опубликовать другой редактор. Более сложный сервис добавляет собственный идентификатор запроса и правило повторного принятия одного действия. Это отдельная прикладная задача, которую нужно проектировать вместе с транзакцией.
Представим, что вместе с публикацией записывается платное списание. Простое повторение команды после потери ответа уже меняет деньги дважды. Поэтому причина повторения должна быть названа: исправление неверных данных, новая пользовательская попытка и повтор неизвестного результата не являются одним случаем. Наш пример не создаёт таблицу таких операций, но показывает, где начинается необходимость такого договора.
Письмо о публикации также отправляется после подтверждённого результата либо через отдельно спроектированную очередь согласованных событий. Пользовательское сообщение «операция обрабатывается» может быть честнее немедленного обещания успеха при неизвестной фиксации. Граница COMMIT полезна только когда приложение правильно трактует все наблюдаемые исходы этой границы.
Граница времени работы
Транзакцию открывают на короткое согласованное изменение, а не на время, пока человек выбирает название в форме. Длительное ожидание пользователя удерживает контекст базы и может мешать другим изменениям. Сначала собирают и проверяют пользовательское намерение, затем читают необходимое актуальное состояние и выполняют ограниченный блок.
Для редактирования давно открытой формы дальше рассмотрим проверку версии объекта. Она помогает отличить свежие данные от устаревших без удержания блокировки всё время просмотра страницы. Транзакция и версия решают связанные, но разные задачи: согласовать несколько команд и определить, на каком состоянии основывается действие.
Прочитайте при будущем ручном повторении две выборки до и после отката. Их условия одинаковы, но наблюдаемое состояние различается по границе фиксации. Теперь операция публикации имеет понятные обязательные шаги, явное решение о сохранении и ограниченную область действия, вместо надежды, что несколько отдельных команд обязательно завершатся вместе.