Obsługa błędów i transakcji w triggerach: EXCEPTION, RAISE
Czasem triggery zachowują się nieprzewidywalnie i może to być związane z:
- Błędem logicznym w logice funkcji podpiętej do triggera.
- Naruszeniem ograniczeń bazy danych (np. naruszenie unikalności albo niezgodność typów danych).
- Problemami w transakcjach, kiedy trigger powoduje rollback zmian przez błąd.
- Rekursją, jeśli trigger wywołuje sam siebie (często przypadkowo).
Żeby zapobiegać takim problemom, PostgreSQL daje możliwość obsługi błędów wewnątrz triggerów i ich funkcji. Te narzędzia to bloki EXCEPTION i operator RAISE, które dziś ogarniemy na przykładach.
Obsługa błędów przez blok EXCEPTION
Blok EXCEPTION pozwala nam przechwycić błędy i wykonać jakiś kod do ich obsługi. To trochę jak try-catch w językach programowania, takich jak Python czy Java.
Blok EXCEPTION używa się w funkcjach PL/pgSQL tak:
BEGIN
-- Główny kod funkcji
EXCEPTION
WHEN <typ_bledu> THEN
-- Kod obsługi błędu
END;
Gdzie <typ_bledu> — to konkretny błąd albo grupa błędów, które chcesz obsłużyć (np. unique_violation, division_by_zero itd.).
Przykład: logowanie błędów w triggerach
Wyobraź sobie, że mamy tabelę logs, gdzie chcemy zapisywać błędy pojawiające się przy insercie do tabeli students. Oto przykład:
Tworzymy tabelę do logów
CREATE TABLE logs (
id SERIAL PRIMARY KEY,
error_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
error_message TEXT
);
Tworzymy funkcję z obsługą błędów
CREATE OR REPLACE FUNCTION track_insert_errors()
RETURNS TRIGGER AS $$
BEGIN
-- Próbujemy wykonać główny kod
BEGIN
-- Przykład "błędnej" akcji: dzielenie przez 0
PERFORM 1 / (NEW.some_value - NEW.some_value);
EXCEPTION
WHEN division_by_zero THEN
-- Jeśli był błąd dzielenia przez 0, zapisujemy go do logów
INSERT INTO logs (error_message) VALUES ('Błąd dzielenia przez 0 przy insercie do students');
END;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Tworzymy trigger
CREATE TRIGGER before_insert_students
BEFORE INSERT ON students
FOR EACH ROW
EXECUTE FUNCTION track_insert_errors();
Teraz, jeśli przy insercie do tabeli students pojawi się błąd dzielenia przez zero, zostanie on obsłużony, a info o nim zapisze się do tabeli logs.
Użycie RAISE do diagnostyki i debugowania
Operator RAISE pozwala wypisywać komunikaty o ostrzeżeniach, błędach albo info do debugowania. To mega przydatne, gdy próbujesz ogarnąć, jak działa (albo nie działa!) twój trigger.
Typy komunikatów RAISE:
DEBUG— komunikat do debugowania.NOTICE— zwykły komunikat informacyjny.WARNING— ostrzeżenie.EXCEPTION— komunikat o błędzie, który kończy działanie funkcji.
Składnia RAISE:
RAISE <typ_komunikatu> 'Komunikat';
Możesz też przekazywać wartości zmiennych:
RAISE NOTICE 'Wartość NEW.id = %', NEW.id;
Przykład: debugowanie wartości w triggerze
Załóżmy, że pojawia się błąd przy update w tabeli students i chcesz wiedzieć, jakie wartości NEW i OLD powodują problem. Do tego użyjemy RAISE:
CREATE OR REPLACE FUNCTION debug_student_update()
RETURNS TRIGGER AS $$
BEGIN
RAISE NOTICE 'OLD.id = %, NEW.id = %', OLD.id, NEW.id;
-- Przykład warunku powodującego błąd:
IF NEW.some_field IS NULL THEN
RAISE EXCEPTION 'Pole some_field nie może być NULL';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER after_update_students
AFTER UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION debug_student_update();
Teraz przy każdym update rekordu zobaczysz wartości OLD i NEW, a także dostaniesz jasny komunikat o błędzie, jeśli się pojawi.
Transakcje w triggerach
Triggery wykonują się w kontekście transakcji. To znaczy, że jeśli gdziekolwiek w triggerze albo jego funkcji pojawi się błąd, cała transakcja zostanie cofnięta. To fajnie chroni bazę przed częściowymi zmianami.
Ale to zachowanie czasem powoduje problemy:
- Jeśli błąd w triggerze jest przez złe dane, to fajnie byłoby cofnąć tylko część akcji.
- Trzeba pamiętać, że rollback transakcji obejmuje nie tylko trigger, ale całą operację, która go wywołała.
Przykład: użycie transakcji w triggerze
Dla przykładu, wyobraź sobie, że chcemy wykonać jakąś logikę biznesową, która obejmuje dwie akcje: update w tabeli students i zapis loga do logs. Jeśli jedna z tych akcji się nie uda, cała transakcja się cofa.
CREATE OR REPLACE FUNCTION transactional_student_update()
RETURNS TRIGGER AS $$
BEGIN
-- Logowanie próby update
INSERT INTO logs (error_message) VALUES ('Próba update studenta z id ' || NEW.id);
-- Sprawdzamy warunki biznesowe
IF NEW.some_value IS NULL THEN
RAISE EXCEPTION 'Pole some_value nie może być NULL';
END IF;
-- Jeśli wszystko OK, zwracamy NEW
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER before_update_students
BEFORE UPDATE ON students
FOR EACH ROW
EXECUTE FUNCTION transactional_student_update();
Typowe błędy przy pracy z triggerami i jak ich unikać
Często spotykane błędy programistów:
Rekursywne triggery. To się dzieje, gdy trigger robi zmiany, które znowu go wywołują. Przykład rozwiązania: użyj warunku WHEN albo dodaj flagę, żeby nie wywoływać się ponownie.
Rollback całej transakcji przez błędy. Często to nie jest pożądane, jeśli trigger nie jest bezpośrednio związany z głównymi danymi. Przykład rozwiązania: mądrze używaj bloków EXCEPTION.
Za dużo info do debugowania. Zaśmieca logi i utrudnia analizę. Przykład rozwiązania: używaj RAISE tylko podczas developmentu i testów.
Spadek wydajności. Złożone triggery mogą spowolnić operacje INSERT, UPDATE albo DELETE. Przykład rozwiązania: minimalizuj logikę triggera i unikaj zapytań, które mocno obciążają bazę.
GO TO FULL VERSION