Помилки в PostgreSQL можуть виникати з купи причин: порушення обмежень (NOT NULL, UNIQUE, CHECK), синтаксичні помилки, дублікати значень і т.д. Якщо не перехоплювати і не обробляти такі помилки, вся зовнішня транзакція може бути повністю відкотана. Для стійких бізнес-операцій важливо правильно їх обробляти.
PL/pgSQL дає потужний механізм блоків BEGIN ... EXCEPTION ... END для перехоплення і обробки помилок у функціях і процедурах. Вони схожі на try-catch з Python або Java, але мають свої фішки в контексті транзакцій PostgreSQL.
Важливий момент:
Кожен блок BEGIN ... EXCEPTION ... END працює як “віртуальний savepoint”. Якщо виникає виключення — всі зміни цього блоку автоматично відкочуються. Це єдиний коректний спосіб часткового відкату у функціях і процедурах на PL/pgSQL
Синтаксис обробки помилок за допомогою EXCEPTION
BEGIN
-- основний код
EXCEPTION
WHEN ТИП_ПОМИЛКИ THEN
-- обробка конкретної помилки
WHEN ІНШИЙ_ТИП_ПОМИЛКИ THEN
-- інший обробник
WHEN OTHERS THEN
-- обробка всіх інших помилок
END;
Ілюстрація у функціях/процедурах
DO $$
BEGIN
RAISE NOTICE 'Зараз буде помилка...';
PERFORM 1 / 0;
EXCEPTION
WHEN division_by_zero THEN
RAISE NOTICE 'Ділення на нуль перехоплено!';
END;
$$;
Приклад: оновлення з обробкою і відкатом помилок
Нехай є таблиця замовлень:
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
amount NUMERIC NOT NULL,
status TEXT NOT NULL
);
Створимо функцію, яка оновлює статус замовлення і при помилці нічого не змінює:
CREATE OR REPLACE FUNCTION update_order_status(order_id INT, new_status TEXT)
RETURNS VOID AS $$
BEGIN
BEGIN
UPDATE orders
SET status = new_status
WHERE id = order_id;
-- Симуляція помилки
IF new_status = 'FAIL' THEN
RAISE EXCEPTION 'Симулюємо помилку!';
END IF;
RAISE NOTICE 'Статус замовлення % оновлено', order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Помилка при оновленні замовлення %: %', order_id, SQLERRM;
-- Всі зміни всередині блоку автоматично відкочуються!
-- Повторно кидаємо помилку для зовнішньої обробки, якщо треба
RAISE;
END;
END;
$$ LANGUAGE plpgsql;
Як це працює у процедурах з явним керуванням транзакціями
У процедурах (CREATE PROCEDURE) можна використовувати COMMIT, ROLLBACK, SAVEPOINT, але не можна робити ROLLBACK TO SAVEPOINT. Якщо треба відкочувати тільки частину операцій всередині процедури, використовуй все той же BEGIN ... EXCEPTION ... END:
CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
-- Вся процедура може використовувати COMMIT/ROLLBACK, але для відкату окремого етапу:
BEGIN
UPDATE accounts
SET balance = balance - amount
WHERE id = (SELECT account_id FROM orders WHERE id = order_id);
-- помилка
IF amount < 0 THEN
RAISE EXCEPTION 'Сума не може бути від’ємною!';
END IF;
UPDATE orders SET status = 'PAID' WHERE id = order_id;
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Помилка при обробці оплати замовлення %: %', order_id, SQLERRM;
-- зміни в цьому блоці відкочуються автоматично
END;
COMMIT; -- Можна явно завершувати транзакцію тільки у процедурах!
END;
$$;
Логування помилок
Важливо не просто перехоплювати помилки, а й зберігати їх для подальшого аналізу.
CREATE TABLE error_log (
id SERIAL PRIMARY KEY,
order_id INT,
error_message TEXT,
error_time TIMESTAMP DEFAULT now()
);
У функції чи процедурі:
EXCEPTION
WHEN OTHERS THEN
INSERT INTO error_log (order_id, error_message)
VALUES (order_id, SQLERRM);
RAISE NOTICE 'Помилка занесена в журнал: %', SQLERRM;
RAISE;
Важливі обмеження і нюанси в PostgreSQL 17
Всередині функцій (CREATE FUNCTION ... ) команди транзакційного керування (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT) використовувати не можна! Всі функції виконуються цілком у рамках зовнішньої транзакції.
Всередині процедур (CREATE PROCEDURE ... ) можна явно писати SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. АЛЕ: ROLLBACK TO SAVEPOINT — ЗАБОРОНЕНО! (Отримаєш синтаксичну помилку, якщо спробуєш використати ROLLBACK TO SAVEPOINT у PL/pgSQL-процедурі).
Відкат "частини коду" всередині функцій і процедур робиться за допомогою блоків BEGIN ... EXCEPTION ... END. При виникненні помилки все всередині блоку відкочується автоматично, і виконання може йти далі.
Процедури (CREATE PROCEDURE) не можна запускати всередині функції чи через SELECT — тільки окремою командою CALL ....
Як реально реалізувати "частковий відкат" у PL/pgSQL?
Єдиний робочий спосіб — використовувати обробку помилок через блоки BEGIN ... EXCEPTION ... END. Такий блок автоматично створює точку збереження, і при помилці відкочує зміни всередині блоку, не чіпаючи іншу частину процедури/функції.
Приклад з EXCEPTION (рекомендований підхід):
CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
-- Якийсь код
BEGIN
-- Тут помилка не призведе до відкату всієї процедури,
-- тільки цього блоку!
INSERT INTO demo VALUES ('bad data'); -- можливо, викличе помилку
EXCEPTION
WHEN OTHERS THEN
RAISE NOTICE 'Помилка оброблена, зміни всередині блоку скасовані';
END;
-- Тут виконання продовжується!
END;
$$;
Приклад: завантаження пакету даних із захистом від відкату всього процесу
CREATE OR REPLACE PROCEDURE load_big_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM import_table LOOP
BEGIN
INSERT INTO target_table (col1, col2)
VALUES (rec.col1, rec.col2);
EXCEPTION WHEN OTHERS THEN
INSERT INTO import_errors (err_msg)
VALUES ('Помилка у записі: ' || rec.col1 || ': ' || SQLERRM);
-- зміни всередині цього блоку скасовані!
END;
END LOOP;
COMMIT; -- дозволено тільки якщо процедура запущена поза явною зовнішньою транзакцією!
END;
$$;
-- Виклик процедури
CALL load_big_batch();
Зверни увагу: якщо викликати таку процедуру з клієнта, у якого вже почата транзакція (наприклад, Python з autocommit=False), виконання COMMIT або SAVEPOINT всередині процедури викличе помилку.
Поради щодо роботи з вкладеними точками збереження і EXCEPTION
- Не використовуй ROLLBACK TO SAVEPOINT у PL/pgSQL! Це викличе синтаксичну помилку.
- Для часткового відкату завжди використовуй вкладені блоки BEGIN ... EXCEPTION ... END.
- Не забувай, що COMMIT і ROLLBACK всередині процедур перезапускають транзакцію — їх можна викликати тільки коли процедура запущена в autocommit-режимі!
- Логуй помилки в окрему таблицю, щоб не втрачати інформацію про некоректні рядки.
- Якщо бізнес-операція має бути строго атомарною (все-або-нічого) — роби її функцією, без COMMIT/ROLLBACK всередині; якщо потрібна поетапна обробка — реалізуй як процедуру.
Приклад: імпорт по пакетах з частковою обробкою
CREATE OR REPLACE PROCEDURE import_batch()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT * FROM staging_table LOOP
BEGIN
INSERT INTO data_table (data)
VALUES (rec.data);
EXCEPTION
WHEN unique_violation THEN
INSERT INTO import_log (msg)
VALUES ('Дубль: ' || rec.data);
WHEN OTHERS THEN
INSERT INTO import_log (msg)
VALUES ('Помилка: ' || rec.data || ' — ' || SQLERRM);
END;
END LOOP;
END;
$$;
Головне, що треба запам’ятати для PostgreSQL 17:
У PL/pgSQL процедурах дозволені SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, але ROLLBACK TO SAVEPOINT — не можна.
"Частковий відкат" всередині функцій і процедур реалізується тільки за допомогою вкладених блоків BEGIN ... EXCEPTION ... END.
Транзакціями краще керувати ззовні (через параметри підключення і autocommit), а всередині процедур — використовувати описані механізми обробки помилок.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ