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
- Nie używaj ROLLBACK TO SAVEPOINT w PL/pgSQL! To wywoła błąd składni.
- Do częściowego rollbacku zawsze używaj zagnieżdżonych bloków BEGIN ... EXCEPTION ... END.
- Pamiętaj, że COMMIT i ROLLBACK w procedurach restartują transakcję — można ich używać tylko gdy procedura leci w trybie autocommit!
- Loguj błędy do osobnej tabeli, żeby nie zgubić info o niepoprawnych rekordach.
- 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.
GO TO FULL VERSION