История вопроса
В операциях обработки бизнес-логики зачастую требуется менять данные в нескольких связанных таблицах. Если что-то неудачно завершилось (например, из-за нарушенного ограничения), важно отменить не только последнюю операцию, а все действия в рамках логики. Для этого применяется механика транзакций и обработка ошибок.
Проблема
Не все СУБД по умолчанию обеспечивают откат при ошибке в многошаговых процедурах (особенно если используются TRY/CATCH, EXCEPTION-обработчики). Неправильное написание транзакционной логики приводит к «кусочным» изменениям (часть данных обновилась, часть — нет), что компрометирует бизнес-целостность.
Решение
Для корректной обработки используются явное открытие и завершение транзакций (BEGIN TRANSACTION, COMMIT/ROLLBACK) с отловом ошибок. В разных СУБД синтаксис и возможности различаются.
Пример для MS SQL Server:
CREATE PROCEDURE update_balances(@from INT, @to INT, @amount DECIMAL(10,2)) AS BEGIN BEGIN TRY BEGIN TRANSACTION; UPDATE accounts SET balance = balance - @amount WHERE id = @from; UPDATE accounts SET balance = balance + @amount WHERE id = @to; COMMIT TRANSACTION; END TRY BEGIN CATCH ROLLBACK TRANSACTION; INSERT INTO error_log(err_message, err_date) VALUES(ERROR_MESSAGE(), GETDATE()); THROW; -- пробрасываем ошибку дальше END CATCH END;
Для PostgreSQL:
CREATE OR REPLACE FUNCTION update_balances(from_id INT, to_id INT, amount NUMERIC) RETURNS void AS $$ BEGIN UPDATE accounts SET balance = balance - amount WHERE id = from_id; UPDATE accounts SET balance = balance + amount WHERE id = to_id; -- Исключение будет автоматически откатывать транзакцию EXCEPTION WHEN OTHERS THEN INSERT INTO error_log(err_message, err_date) VALUES(SQLERRM, NOW()); RAISE; END; $$ LANGUAGE plpgsql;
Ключевые особенности:
Может ли ошибка внутри хранимой процедуры всегда автоматически откатывать transaction? Нет! Например, в MS SQL Server такой откат не гарантирован — требуется явный ROLLBACK. В PostgreSQL при любой ошибке вся транзакция становится "испорченной" и требует завершения.
Можно ли "частично" закоммитить внутри одной процедуры? Плохая практика. Рекомендуется делать commit/rollback только один раз в конце бизнес-операции. Часть платформ позволяет "savepoints", но это для спецзадач.
Какие типы ошибок не поддаются ловле через TRY/CATCH/EXCEPTION в SQL? Некоторые системные сбои (например, разрыв соединения с сервером) не попадут в обработчик и могут сделать откат невозможным.
Хранимая процедура обновляла три таблицы и завершалась COMMIT без отлова ошибок. В результате при ошибке во второй таблице первая уже была изменена, и пришлось часами "откатывать" данные вручную из бекапов.
Плюсы:
Просто и "работает", пока не случится сбой.
Минусы:
Риск неконсистентности; невозможно быстро восстановиться при ошибках.
Внедрили явную обработку ошибок + rollback транзакций с записью всех сбоев в отдельный log. Возврат к целостному состоянию базы занимает секунды, сбои анализируются и минимизируются.
Плюсы:
Гарантия неизменности мытых данных даже при сбоях; прозрачная логика.
Минусы:
Слегка сложнее для начинающих, требует дисциплины в коде.