CodeGym /Cours /SQL SELF /Gestion des erreurs et retour à l'état initial : EXCEPTIO...

Gestion des erreurs et retour à l'état initial : EXCEPTION, RAISE

SQL SELF
Niveau 53 , Leçon 3
Disponible

Les erreurs dans PostgreSQL peuvent arriver pour plein de raisons : violation de contraintes (NOT NULL, UNIQUE, CHECK), erreurs de syntaxe, valeurs dupliquées, etc. Si tu ne les interceptes pas et ne les gères pas, toute la transaction externe peut être rollbackée complètement. Pour des opérations business fiables, c'est super important de bien les gérer.

PL/pgSQL te file un mécanisme puissant avec les blocs BEGIN ... EXCEPTION ... END pour intercepter et gérer les erreurs dans les fonctions et procédures. C'est un peu comme try-catch en Python ou Java, mais avec des particularités liées aux transactions PostgreSQL.

Point important :

Chaque bloc BEGIN ... EXCEPTION ... END agit comme un “savepoint virtuel”. Si une exception se produit, tous les changements dans ce bloc sont automatiquement annulés. C'est la seule façon correcte de faire un rollback partiel dans les fonctions et procédures PL/pgSQL.

Syntaxe de gestion des erreurs avec EXCEPTION

BEGIN
    -- code principal
EXCEPTION
    WHEN TYPE_ERREUR THEN
        -- gestion d'une erreur spécifique
    WHEN AUTRE_TYPE_ERREUR THEN
        -- autre gestionnaire
    WHEN OTHERS THEN
        -- gestion de toutes les autres erreurs
END;

Illustration dans les fonctions/procédures

DO $$
BEGIN
    RAISE NOTICE 'Une erreur va arriver...';
    PERFORM 1 / 0;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE 'Division par zéro interceptée !';
END;
$$;

Exemple : update avec gestion et rollback des erreurs

Imaginons qu'on a une table de commandes :

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

On va créer une fonction qui met à jour le statut d'une commande et, en cas d'erreur, ne change rien :

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;

        -- Simulation d'une erreur
        IF new_status = 'FAIL' THEN
            RAISE EXCEPTION 'On simule une erreur !';
        END IF;

        RAISE NOTICE 'Statut de la commande % mis à jour', order_id;

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Erreur lors de la mise à jour de la commande % : %', order_id, SQLERRM;
            -- Tous les changements dans ce bloc seront annulés automatiquement !
            -- On relance l'erreur pour gestion externe si besoin
            RAISE;
    END;
END;
$$ LANGUAGE plpgsql;

Comment ça marche dans les procédures avec gestion explicite des transactions

Dans les procédures (CREATE PROCEDURE), tu peux utiliser COMMIT, ROLLBACK, SAVEPOINT, mais tu ne peux pas faire ROLLBACK TO SAVEPOINT. Si tu veux rollback juste une partie des opérations dans la procédure, utilise toujours BEGIN ... EXCEPTION ... END :

CREATE OR REPLACE PROCEDURE pay_order(order_id INT, amount NUMERIC)
LANGUAGE plpgsql
AS $$
BEGIN
    -- Toute la procédure peut utiliser COMMIT/ROLLBACK, mais pour rollback une étape précise :
    BEGIN
        UPDATE accounts
        SET balance = balance - amount
        WHERE id = (SELECT account_id FROM orders WHERE id = order_id);

        -- erreur
        IF amount < 0 THEN
            RAISE EXCEPTION 'Le montant ne peut pas être négatif !';
        END IF;

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

    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Erreur lors du traitement du paiement de la commande % : %', order_id, SQLERRM;
            -- les changements dans ce bloc seront annulés automatiquement
    END;

    COMMIT; -- Tu peux finir explicitement la transaction seulement dans les procédures !
END;
$$;

Logging des erreurs

C'est important non seulement d'intercepter les erreurs, mais aussi de les enregistrer pour analyse plus tard.

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

Dans une fonction ou une procédure :

EXCEPTION
    WHEN OTHERS THEN
        INSERT INTO error_log (order_id, error_message)
        VALUES (order_id, SQLERRM);
        RAISE NOTICE 'Erreur enregistrée dans le log : %', SQLERRM;
        RAISE;

Limitations et subtilités importantes dans PostgreSQL 17

Dans les fonctions (CREATE FUNCTION ... ), tu ne peux pas utiliser les commandes de gestion de transaction (BEGIN, COMMIT, ROLLBACK, SAVEPOINT, ROLLBACK TO SAVEPOINT, RELEASE SAVEPOINT) ! Toutes les fonctions s'exécutent entièrement dans la transaction externe.

Dans les procédures (CREATE PROCEDURE ... ), tu peux explicitement écrire SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK. MAIS : ROLLBACK TO SAVEPOINT — INTERDIT ! (Tu auras une erreur de syntaxe si tu essaies d'utiliser ROLLBACK TO SAVEPOINT dans une procédure PL/pgSQL).

Le rollback "d'une partie du code" dans les fonctions et procédures se fait avec les blocs BEGIN ... EXCEPTION ... END. Si une erreur arrive, tout dans le bloc est annulé automatiquement, et l'exécution peut continuer.

Les procédures (CREATE PROCEDURE) ne peuvent pas être lancées dans une fonction ou via SELECT — seulement avec la commande CALL ... à part.

Comment vraiment faire un "rollback partiel" en PL/pgSQL ?

La seule façon qui marche — c'est de gérer les erreurs avec les blocs BEGIN ... EXCEPTION ... END. Ce bloc crée automatiquement un point de sauvegarde, et en cas d'erreur, il annule les changements à l'intérieur, sans toucher au reste de la procédure/fonction.

Exemple avec EXCEPTION (méthode recommandée) :

CREATE OR REPLACE PROCEDURE demo_savepoint()
LANGUAGE plpgsql
AS $$
BEGIN
    -- Un peu de code
    BEGIN
        -- Ici, une erreur ne va pas rollback toute la procédure,
        -- juste ce bloc !
        INSERT INTO demo VALUES ('mauvaises données'); -- peut-être va causer une erreur
    EXCEPTION
        WHEN OTHERS THEN
            RAISE NOTICE 'Erreur gérée, changements dans le bloc annulés';
    END;
    -- Ici, l'exécution continue !
END;
$$;

Exemple : chargement d'un batch de données avec protection contre le rollback total

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 ('Erreur dans la ligne : ' || rec.col1 || ' : ' || SQLERRM);
            -- changements dans ce bloc annulés !
        END;
    END LOOP;
    COMMIT;  -- ok seulement si la procédure est lancée hors d'une transaction externe explicite !
END;
$$;

-- Appel de la procédure
CALL load_big_batch();

À noter : si tu appelles cette procédure depuis un client qui a déjà démarré une transaction (genre Python avec autocommit=False), un COMMIT ou SAVEPOINT dans la procédure va causer une erreur.

Conseils pour gérer les savepoints imbriqués et EXCEPTION

  1. N'utilise pas ROLLBACK TO SAVEPOINT dans PL/pgSQL ! Ça va causer une erreur de syntaxe.
  2. Pour rollback partiel, utilise toujours des blocs imbriqués BEGIN ... EXCEPTION ... END.
  3. N'oublie pas que COMMIT et ROLLBACK dans les procédures redémarrent la transaction — tu peux les utiliser seulement si la procédure tourne en mode autocommit !
  4. Loggue les erreurs dans une table à part pour ne pas perdre d'infos sur les lignes incorrectes.
  5. Si une opération business doit être strictement atomique (tout ou rien) — fais-en une fonction, sans COMMIT/ROLLBACK dedans ; si tu veux gérer étape par étape — fais-en une procédure.

Exemple : import par batch avec gestion partielle

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

À retenir absolument pour PostgreSQL 17 :

Dans les procédures PL/pgSQL, tu peux utiliser SAVEPOINT, RELEASE SAVEPOINT, COMMIT, ROLLBACK, mais ROLLBACK TO SAVEPOINT — interdit.

Le "rollback partiel" dans les fonctions et procédures se fait uniquement avec des blocs imbriqués BEGIN ... EXCEPTION ... END.

Il vaut mieux gérer les transactions à l'extérieur (via les paramètres de connexion et autocommit), et à l'intérieur des procédures — utiliser les mécanismes de gestion d'erreur décrits ici.

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