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

Синтаксис обробки помилок за допомогою 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

  1. Не використовуй 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 процедурах дозволені SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, але ROLLBACK TO SAVEPOINT — не можна.

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

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

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