JavaRush /Курси /SQL SELF /Обробка помилок і повернення до початкового стану: EXCEPT...

Обробка помилок і повернення до початкового стану: EXCEPTION, RAISE

SQL SELF
Рівень 53 , Лекція 3
Відкрита

Помилки в PostgreSQL можуть виникати з купи причин: порушення обмежень (NOT NULL, UNIQUE, CHECK), синтаксичні помилки, дублікати значень і т.д. Якщо не перехоплювати і не обробляти такі помилки, вся зовнішня транзакція може бути повністю відкотана. Для стійких бізнес-операцій важливо правильно їх обробляти.

PL/pgSQL дає потужний механізм блоків BEGIN ... EXCEPTION ... END для перехоплення і обробки помилок у функціях і процедурах. Вони схожі на try-catch з Python або Java, але мають свої фішки в контексті транзакцій PostgreSQL.

Важливий момент:

Кожен блок BEGIN ... EXCEPTION ... END працює як “віртуальний savepoint”. Якщо виникає виключення — всі зміни цього блоку автоматично відкочуються. Це єдиний коректний спосіб часткового відкату у функціях і процедурах на PL/pgSQL.

ВАЖЛИВО: це не безкоштовно. Кожен BEGIN з обробником EXCEPTION створює subtransaction, і вхід/вихід з такого блоку помітно дорожчий за звичайний блок без EXCEPTION. Не обгортай кожен INSERT у EXCEPTION «про всяк випадок» — використовуй лише там, де справді потрібен частковий відкат.

Синтаксис обробки помилок за допомогою 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, RELEASE SAVEPOINT і ROLLBACK TO SAVEPOINT у PL/pgSQL заборонені повністю — для часткового відкату всередині процедури використовуй все той же 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 ... ) можна явно писати тільки COMMIT і ROLLBACK. Команди SAVEPOINT, RELEASE 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 всередині процедури викличе помилку.

Поради щодо роботи з вкладеними точками збереження і EXCEPTION

  1. Не використовуй SAVEPOINT, RELEASE SAVEPOINT і ROLLBACK TO SAVEPOINT у PL/pgSQL! Усі три команди заборонені й у функціях, і в процедурах — будь-яка з них дасть синтаксичну помилку.
  2. Для часткового відкату завжди використовуй вкладені блоки BEGIN ... EXCEPTION ... END.
  3. Не забувай, що COMMIT і ROLLBACK всередині процедур перезапускають транзакцію — їх можна викликати тільки коли процедура запущена в autocommit-режимі!
  4. Логуй помилки в окрему таблицю, щоб не втрачати інформацію про некоректні рядки.
  5. Якщо бізнес-операція має бути строго атомарною (все-або-нічого) — роби її функцією, без 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 процедурах дозволені тільки COMMIT і ROLLBACK; SAVEPOINT, RELEASE SAVEPOINT і ROLLBACK TO SAVEPOINT заборонені повністю.

"Частковий відкат" всередині функцій і процедур реалізується тільки за допомогою вкладених блоків BEGIN ... EXCEPTION ... END.

Транзакціями краще керувати ззовні (через параметри підключення і autocommit), а всередині процедур — використовувати описані механізми обробки помилок.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ