CodeGym /Kursy /SQL SELF /Obsługa błędów i powrót do stanu początkowego: EXCEPTION,...

Obsługa błędów i powrót do stanu początkowego: EXCEPTION, RAISE

SQL SELF
Poziom 53 , Lekcja 3
Dostępny

Błędy w PostgreSQL mogą się pojawić z różnych powodów: naruszenie ograniczeń (NOT NULL, UNIQUE, CHECK), błędy składni, duplikaty wartości itd. Jeśli nie przechwycisz i nie obsłużysz takich błędów, cała zewnętrzna transakcja może zostać cofnięta. Dla stabilnych operacji biznesowych ważne jest, żeby dobrze je obsługiwać.

PL/pgSQL daje mocny mechanizm bloków BEGIN ... EXCEPTION ... END do przechwytywania i obsługi błędów w funkcjach i procedurach. Są podobne do try-catch z Pythona albo Javy, ale mają swoje specyficzne zasady w kontekście transakcji PostgreSQL.

Ważna sprawa:

Każdy blok BEGIN ... EXCEPTION ... END działa jak “wirtualny savepoint”. Jeśli pojawi się wyjątek — wszystkie zmiany w tym bloku są automatycznie cofane. To jedyny poprawny sposób częściowego rollbacku w funkcjach i procedurach PL/pgSQL

Składnia obsługi błędów z EXCEPTION

BEGIN
    -- główny kod
EXCEPTION
    WHEN TYP_BŁĘDU THEN
        -- obsługa konkretnego błędu
    WHEN INNY_TYP_BŁĘDU THEN
        -- inny handler
    WHEN OTHERS THEN
        -- obsługa wszystkich pozostałych błędów
END;

Ilustracja w funkcjach/procedurach

DO $$
BEGIN
    RAISE NOTICE 'Zaraz będzie błąd...';
    PERFORM 1 / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'Dzielenie przez zero przechwycone!';
END;
$$;

Przykład: update z obsługą i rollbackiem błędów

Załóżmy, że mamy tabelę zamówień:

CREATE TABLE orders (
    id SERIAL PRIMARY KEY,
    amount NUMERIC NOT NULL,
    status TEXT NOT NULL
);

Zróbmy funkcję, która aktualizuje status zamówienia i w razie błędu nic nie zmienia:

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;

        -- Symulacja błędu
        IF new_status = 'FAIL' THEN
            RAISE EXCEPTION 'Symulujemy błąd!';
        END IF;

        RAISE NOTICE 'Status zamówienia % zaktualizowany', order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Błąd przy aktualizacji zamówienia %: %', order_id, SQLERRM;
            -- Wszystkie zmiany w tym bloku zostaną automatycznie cofnięte!
            -- Ponownie rzucamy błąd do zewnętrznej obsługi, jeśli trzeba
            RAISE;
    END;
END;
$$ LANGUAGE plpgsql;

Jak to działa w procedurach z jawnym zarządzaniem transakcjami

W procedurach (CREATE PROCEDURE) możesz używać COMMIT, ROLLBACK, SAVEPOINT, ale nie możesz robić ROLLBACK TO SAVEPOINT. Jeśli chcesz cofnąć tylko część operacji w procedurze, użyj znowu BEGIN ... EXCEPTION ... END:

CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    -- Cała procedura może używać COMMIT/ROLLBACK, ale do rollbacku pojedynczego etapu:
    BEGIN
        UPDATE accounts
        SET balance = balance - amount
        WHERE id = (SELECT account_id FROM orders WHERE id = order_id);

        -- błąd
        IF amount < 0 THEN
            RAISE EXCEPTION 'Kwota nie może być ujemna!';
        END IF;

        UPDATE orders SET status = 'PAID' WHERE id = order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Błąd przy obsłudze płatności zamówienia %: %', order_id, SQLERRM;
            -- zmiany w tym bloku zostaną automatycznie cofnięte
    END;

    COMMIT; -- Jawne zakończenie transakcji tylko w procedurach!
END;
$$;

Logowanie błędów

Ważne jest nie tylko przechwytywać błędy, ale też je zapisywać do późniejszej analizy.

CREATE TABLE error_log (
    id SERIAL PRIMARY KEY,
    order_id INT,
    error_message TEXT,
    error_time TIMESTAMP DEFAULT now()
);

W funkcji albo procedurze:

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (order_id, error_message)
        VALUES (order_id, SQLERRM);
        RAISE NOTICE 'Błąd zapisany do loga: %', SQLERRM;
        RAISE;

Ważne ograniczenia i niuanse w PostgreSQL 17

W funkcjach (CREATE FUNCTION ... ) nie możesz używać poleceń zarządzania transakcją (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT)! Wszystkie funkcje wykonują się w całości w ramach zewnętrznej transakcji.

W procedurach (CREATE PROCEDURE ... ) możesz jawnie pisać SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. ALE: ROLLBACK TO SAVEPOINT — ZABRONIONE! (Dostaniesz błąd składni, jeśli spróbujesz użyć ROLLBACK TO SAVEPOINT w procedurze PL/pgSQL).

Rollback "części kodu" w funkcjach i procedurach robi się przez bloki BEGIN ... EXCEPTION ... END. Gdy pojawi się błąd, wszystko w bloku jest cofane automatycznie i wykonanie może lecieć dalej.

Procedur (CREATE PROCEDURE) nie można odpalać w funkcji ani przez SELECT — tylko osobnym poleceniem CALL ....

Jak naprawdę zrobić "częściowy rollback" w PL/pgSQL?

Jedyny działający sposób — obsługa błędów przez bloki BEGIN ... EXCEPTION ... END. Taki blok automatycznie tworzy savepoint i przy błędzie cofa zmiany w środku, nie ruszając reszty procedury/funkcji.

Przykład z EXCEPTION (zalecane podejście):

CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
    -- Jakiś kod
    BEGIN
        -- Tutaj błąd nie cofnie całej procedury,
        -- tylko ten blok!
        INSERT INTO demo VALUES ('złe dane'); -- może wywołać błąd
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Błąd obsłużony, zmiany w bloku cofnięte';
    END;
    -- Tutaj wykonanie leci dalej!
END;
$$;

Przykład: ładowanie dużej paczki danych z ochroną przed rollbackiem całego procesu

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 ('Błąd w rekordzie: ' || rec.col1 || ': ' || SQLERRM);
            -- zmiany w tym bloku są cofnięte!
        END;
    END LOOP;
    COMMIT;  -- dozwolone tylko jeśli procedura odpalona poza jawną zewnętrzną transakcją!
END;
$$;

-- Wywołanie procedury
CALL load_big_batch();

Zwróć uwagę: jeśli odpalisz taką procedurę z klienta, który już ma rozpoczętą transakcję (np. Python z autocommit=False), wykonanie COMMIT albo SAVEPOINT w procedurze wywali błąd.

Tipy do pracy z zagnieżdżonymi savepointami i EXCEPTION

  1. Nie używaj ROLLBACK TO SAVEPOINT w PL/pgSQL! To wywoła błąd składni.
  2. Do częściowego rollbacku zawsze używaj zagnieżdżonych bloków BEGIN ... EXCEPTION ... END.
  3. Pamiętaj, że COMMIT i ROLLBACK w procedurach restartują transakcję — można ich używać tylko gdy procedura leci w trybie autocommit!
  4. Loguj błędy do osobnej tabeli, żeby nie zgubić info o niepoprawnych rekordach.
  5. Jeśli operacja biznesowa ma być ściśle atomowa (wszystko-albo-nic) — rób ją jako funkcję, bez COMMIT/ROLLBACK w środku; jeśli potrzebujesz etapowej obsługi — zrób jako procedurę.

Przykład: import paczkami z częściową obsługą

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 ('Duplikat: ' || rec.data);
            WHEN OTHERS THEN
                INSERT INTO import_log (msg)
                VALUES ('Błąd: ' || rec.data || ' — ' || SQLERRM);
        END;
    END LOOP;
END;
$$;

Najważniejsze do zapamiętania dla PostgreSQL 17:

W PL/pgSQL procedurach dozwolone są SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, ale ROLLBACK TO SAVEPOINT — nie wolno.

"Częściowy rollback" w funkcjach i procedurach robi się tylko przez zagnieżdżone bloki BEGIN ... EXCEPTION ... END.

Transakcjami najlepiej sterować z zewnątrz (przez parametry połączenia i autocommit), a w środku procedur — używać opisanych mechanizmów obsługi błędów.

Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION