Correction détaillée — Bloc 5 : Triggers
Partie A
1. Identifier les événements à automatiser
On se place dans le modèle :
Client(idc, nom, age)Village(idv, ville, activite, prix, capacite)Sejour(ids, idc, idv, debut, fin)ArchiveSejour(ids, idc, idv, debut, fin, date_archivage)
On liste table × opération × logique métier probable.
Table Client
INSERT- Inscription d’un nouveau client.
- Possible automatisme : initialiser des compteurs dérivés (
nb_sejours = 0), journaliser la création.
UPDATE- Changement de
nom,age. - Peu de logique métier à automatiser côté trigger (les règles sont déjà des contraintes).
- Changement de
DELETE- Suppression d’un client.
- Si la FK
sejour.idc → client.idcest enON DELETE CASCADE, pas besoin de trigger. - Si elle est en
RESTRICT, pas de suppression possible si des séjours existent. - Un trigger pourrait servir uniquement à journaliser (log).
Conclusion : pas de trigger “critique” nécessaire sur client, éventuellement log / statistiques.
Table Village
INSERT- Création d’un nouveau village.
- Automatisable : journalisation, initialisation de colonnes dérivées éventuelles.
UPDATELe cahier des charges impose :
identifiant, ville et prix non modifiables.
Deux options :
- soit on ne donne pas de formulaire qui modifie ces champs,
- soit on met un trigger BEFORE UPDATE qui vérifie que
idv,ville,prixne changent pas et lève une exception sinon.
DELETE- Suppression d’un village.
- Logique métier : interdire tant qu’il existe des séjours associés.
- → Trigger
BEFORE DELETEqui compte les séjours et lève une erreur sicount(*) > 0.
Conclusion : Village est un bon candidat à triggers sur UPDATE (protection des champs non modifiables) et DELETE (protection contre les suppressions dangereuses).
Table Sejour
C’est la table la plus “vivante”.
INSERTRéservation d’un séjour.
Automatisable par triggers :
- contrôle de cohérence des dates (ordre, même année, etc.),
- éventuellement contrôle métier supplémentaire (ex : capacité du village, au-delà de ce qui est déjà géré par contraintes / requêtes),
- mise à jour de compteurs (
client.nb_sejours), - journalisation.
UPDATEModifications de dates, changement de client, de village.
Triggers possibles :
- contrôle des dates (mêmes règles que pour
INSERT), - si
idcchange → ajustement des compteurs par client (−1 / +1), - éventuellement recalculs / logs.
- contrôle des dates (mêmes règles que pour
DELETE- Fonctionnalité 9 et 10 (purge / annulation) entraînent des suppressions.
- Spécification 11 : archivage automatique des séjours supprimés.
- → Trigger
AFTER DELETEqui copie la ligne dansArchiveSejour+ log + mise à jour des compteurs.
Conclusion : Sejour est la table centrale pour les triggers d’archivage, de contrôle et de maintenance de statistiques.
Table ArchiveSejour
INSERT- Toujours déclenché par la logique système (trigger ou procédure).
- En général, aucun trigger (l’archive est un “sink” append-only).
UPDATE/DELETE- À éviter ; pas de trigger.
Conclusion : pas de trigger utile sur ArchiveSejour dans ce scénario.
2. Tracer la chaîne d’actions
On répond en termes de “séquence d’événements” pour les principaux cas.
Cas 1 : purge des séjours obsolètes (fonctionnalité 9 + archivage 11)
L’employé appelle
CALL purge_sejours(:date_limite);(procédure Bloc 3).La procédure exécute un
DELETE FROM sejour WHERE fin < :date_limite RETURNING ...;.Pour chaque ligne supprimée :
Trigger
AFTER DELETE ON sejour:- insère la ligne dans
ArchiveSejour, - écrit éventuellement un message
NOTICE, - met à jour
client.nb_sejourssi ce trigger existe.
- insère la ligne dans
Si une insertion dans
ArchiveSejouréchoue, toute la transaction est annulée (suppression comprise).
Chaîne :
Procédure applicative → DELETE sejour → trigger(s) AFTER DELETE (archivage, stats, log).
Cas 2 : annulation d’un séjour (fonctionnalité 10 + archivage 11)
Version “procédure” + triggers :
L’employé appelle
CALL annuler_sejour(p_ids, p_motif);.La procédure :
- peut éventuellement journaliser le motif via
NOTICEou dans une table dédiée, - exécute un
DELETE FROM sejour WHERE ids = p_ids;.
- peut éventuellement journaliser le motif via
Le
DELETEdéclenche :AFTER DELETE ON sejour→ insertion dansArchiveSejour,- log éventuel,
- mise à jour des compteurs.
Chaîne :
Procédure métier → DELETE sejour → triggers de niveau table (archivage + cohérence).
Cas 3 : tentative de suppression d’un village occupé
Quelqu’un exécute
DELETE FROM village WHERE idv = :idv;.Trigger
BEFORE DELETE ON village:- compte le nombre de séjours associés à
OLD.idv, - si
count > 0,RAISE EXCEPTION→ annule la commande.
- compte le nombre de séjours associés à
Rien n’est supprimé, aucune ligne
Sejourn’est touchée.
Chaîne :
DELETE village → trigger BEFORE DELETE → éventuelle exception → rollback de l’ordre.
Cas 4 : insertion d’un séjour incohérent
Appel direct ou via procédure de réservation :
INSERT INTO sejour(...) VALUES (...);Trigger
BEFORE INSERT ON sejour:- vérifie que
debutetfinsont dans la même année, - éventuellement vérifie la longueur du séjour, etc.
- vérifie que
Si incohérence →
RAISE EXCEPTION→ rollback de l’ordreINSERT.
Chaîne :
INSERT sejour → trigger BEFORE → validation ou exception.
3. Erreurs à lever (RAISE EXCEPTION)
On liste les cas où une exception est légitime (on veut bloquer la transaction) et ceux où un simple message (NOTICE/WARNING) suffit.
Erreurs bloquantes (EXCEPTION)
Séjour multi-année
- Règle : Les séjours sont limités à une année calendaire.
- Trigger
BEFORE INSERT/UPDATE ON sejour→RAISE EXCEPTIONsiEXTRACT(YEAR FROM debut) <> EXTRACT(YEAR FROM fin).
Suppression d’un village occupé
- Règle implicite de cohérence : ne pas perdre des séjours vivants.
- Trigger
BEFORE DELETE ON village→RAISE EXCEPTIONsicount(sejour.idv = OLD.idv) > 0.
Violation explicite d’une règle métier non couverte par contraintes Exemples possibles :
- si on décide de forcer une capacité maximale par trigger (même si déjà gérée par contraintes/exclusion) ;
- si on veut interdire la modification de
villeouprixdansvillagepar unBEFORE UPDATEqui compareOLDetNEWet lève une exception en cas de changement.
Séjour introuvable lors d’une annulation (version plus stricte de Bloc 3)
- Dans
annuler_sejour, si aucunDELETEn’a lieu (IF NOT FOUND), on peutRAISE EXCEPTION 'Séjour introuvable'.
- Dans
Dans tous ces cas : on veut empêcher la persistance de la commande → exception.
Messages non bloquants (NOTICE / WARNING)
Journalisation des opérations normales
- Archivage d’un séjour :
RAISE NOTICE 'Séjour % archivé', OLD.ids; - Insertion/suppression d’un séjour : log dans
log_evenement+NOTICEéventuel.
- Archivage d’un séjour :
Avertissement sur une situation anormale mais acceptable
Séjour très long mais dans l’année (≥ 30 jours) :
RAISE WARNING 'Séjour long (% jours)...', v_nb_jours, NEW.idc;- On accepte la ligne, mais on signale un cas “limite”.
Messages de debug ou de pédagogie
- Affichage d’informations intermédiaires pendant le TD.
En résumé, pour la Partie A :
- Les triggers doivent surtout viser
sejour(INSERT/UPDATE/DELETE) etvillage(DELETE, éventuellement UPDATE). - La chaîne d’actions part toujours des opérations DML (directes ou via procédures) et les triggers injectent automatiquement archivage, contrôle métier et journalisation.
- On réserve
RAISE EXCEPTIONaux violations franches de règles métier (multi-année, suppression d’un village occupé, modification de champs interdits, séjour introuvable), et on gardeNOTICE/WARNINGpour le suivi et les anomalies tolérées.
Partie B
Correction Exercice 1 — Archivage automatique
1. Fonction de trigger archiver_sejour
Objectif : à chaque suppression d’un séjour, copier la ligne supprimée dans ArchiveSejour en ajoutant la date d’archivage.
CREATE OR REPLACE FUNCTION archiver_sejour()
RETURNS trigger AS $$
BEGIN
INSERT INTO archivesejour(ids, idc, idv, debut, fin, date_archivage)
VALUES (OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin, now());
RAISE NOTICE 'Séjour % archivé pour le client % (village %)',
OLD.ids, OLD.idc, OLD.idv;
-- Pour un trigger AFTER DELETE, on renvoie OLD
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Points à vérifier / expliquer :
RETURNS trigger: obligatoire pour toute fonction utilisée comme fonction de trigger.OLDcontient la ligne supprimée (dans un triggerDELETE).- L’
INSERTcopie les valeurs de la ligne supprimée dansArchiveSejouret fixedate_archivageànow()(timestamp actuel). RAISE NOTICE: permet de tracer dans les logs ce qui est archivé (utile en phase de test).RETURN OLD;: convention pour un triggerAFTER DELETE. (EnBEFORE INSERT/UPDATE, on renverrait typiquementNEW.)
2. Création du trigger trg_archive_sejour
Objectif : déclencher la fonction après chaque suppression dans sejour.
CREATE TRIGGER trg_archive_sejour
AFTER DELETE ON sejour
FOR EACH ROW
EXECUTE FUNCTION archiver_sejour();
Explications :
AFTER DELETE: l’archivage se fait après la suppression logique danssejour. Si l’INSERTdansArchiveSejouréchoue, la transaction complète est annulée (suppression comprise).FOR EACH ROW: le trigger se déclenche pour chaque ligne supprimée, pas une seule fois pour l’instruction.EXECUTE FUNCTION archiver_sejour(): syntaxe PostgreSQL moderne (≥ 11).
3. Tests de validation
Scénario minimal reproductible.
- Vérifier la présence d’un séjour à supprimer :
SELECT * FROM sejour WHERE ids = 42;
- Supprimer ce séjour :
DELETE FROM sejour WHERE ids = 42;
On doit voir un message du type :
NOTICE: Séjour 42 archivé pour le client 3 (village 7)
- Vérifier l’archivage :
SELECT *
FROM archivesejour
WHERE ids = 42
ORDER BY date_archivage DESC
LIMIT 1;
Commentaires :
- Si l’
INSERTdansArchiveSejourplante (clés, types, etc.), leDELETEest lui aussi annulé : cohérence forte. - L’archivage est maintenant garanti même si on supprime les séjours via n’importe quel client (psql, appli, script), sans passer par une procédure spécifique.
Correction Exercice 2 — Contrôle de cohérence avant insertion
1. Fonction de trigger controle_sejour_annee
Règle : un séjour doit commencer et se terminer la même année. Sinon, refus.
CREATE OR REPLACE FUNCTION controle_sejour_annee()
RETURNS trigger AS $$
BEGIN
-- Cas 1 : dates nulles → on laisse le soin aux contraintes NOT NULL de gérer
IF NEW.debut IS NULL OR NEW.fin IS NULL THEN
RETURN NEW;
END IF;
-- Cas 2 : incohérence de l'année calendaire
IF EXTRACT(YEAR FROM NEW.debut) <> EXTRACT(YEAR FROM NEW.fin) THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Séjour invalide : début %s et fin %s ne sont pas dans la même année',
NEW.debut, NEW.fin
),
ERRCODE = 'P0001';
END IF;
-- Cas 3 : on laisse passer
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Commentaires techniques
RETURNS trigger: obligatoire pour une fonction de trigger.- Utilisation de
NEW(ligne insérée ou mise à jour). - Vérification explicite des
NULLpour éviter des surprises si les contraintes ne sont pas encore toutes posées. EXTRACT(YEAR FROM …)donne l’année sous forme numérique.RAISE EXCEPTION USING …permet de préciser un message lisible et un code d’erreur.P0001est un code générique “exception utilisateur” côté PL/pgSQL.
2. Création du trigger trg_controle_sejour_annee
On veut contrôler à la fois à l’insertion et à la mise à jour.
CREATE TRIGGER trg_controle_sejour_annee
BEFORE INSERT OR UPDATE ON sejour
FOR EACH ROW
EXECUTE FUNCTION controle_sejour_annee();
Commentaires
BEFORE: le contrôle est fait avant l’écriture en table.INSERT OR UPDATE: couvre création et modification de séjour.FOR EACH ROW: le test est fait pour chaque ligne concernée par l’ordre DML.
3. Jeux de tests
Tester un cas valide
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-10', '2025-07-20');
Attendu : insertion réussie (même année 2025).
Tester un cas invalide
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-12-28', '2026-01-03');
Attendu : exception du type
ERROR: Séjour invalide : début 2025-12-28 et fin 2026-01-03 ne sont pas dans la même année
SQLSTATE: P0001
Tester une mise à jour invalide
UPDATE sejour
SET fin = '2026-01-05'
WHERE ids = <id_d_un_sejour_existant_en_2025>;
Même comportement : levée d’exception, rollback de l’ordre UPDATE.
4. Remarque de conception
Cette règle pourrait aussi être exprimée via un CHECK :
ALTER TABLE sejour
ADD CONSTRAINT ck_sejour_meme_annee
CHECK (EXTRACT(YEAR FROM debut) = EXTRACT(YEAR FROM fin));
Ici, l’exercice vise explicitement l’usage des triggers et de RAISE EXCEPTION.
Le CHECK reste une solution plus simple et plus standard pour cette règle, mais moins pédagogique pour illustrer la partie “Exceptions et triggers”.
Correction Exercice 3 — Refus de suppression d’un village réservé
1. Fonction de trigger interdire_suppression_village
Objectif métier : tant qu’il existe au moins un séjour qui référence un village, ce village ne doit pas être supprimé. On veut en plus un message explicite côté application.
CREATE OR REPLACE FUNCTION interdire_suppression_village()
RETURNS trigger AS $$
DECLARE
v_nb_sejours int;
BEGIN
-- Compter les séjours liés au village qu'on tente de supprimer
SELECT count(*) INTO v_nb_sejours
FROM sejour
WHERE idv = OLD.idv;
IF v_nb_sejours > 0 THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Suppression interdite : % séjours encore liés au village %',
v_nb_sejours, OLD.idv
),
ERRCODE = 'P0002';
END IF;
-- Si aucun séjour ne dépend de ce village, on autorise la suppression
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Points importants :
OLDcontient la ligne devillagequ’on est en train de supprimer.On interroge
sejouravecidv = OLD.idvpour voir si le village est encore utilisé.Si
v_nb_sejours > 0, on lève une exception :- message explicite pour le développeur / l’utilisateur,
ERRCODEcustom (P0002) pour distinguer ce cas des autres erreurs.
Si aucun séjour lié, on retourne simplement
OLDet la suppression se poursuit.
Remarque par rapport au bloc 2 :
- Dans le TD2, la FK
sejour.idv → village.idvest enON DELETE RESTRICT(ouNO ACTION) : la base interdit déjà la suppression si des séjours existent. - Ici, le trigger ajoute un message métier explicite plutôt que de laisser PostgreSQL renvoyer un message générique de contrainte violée.
- Si la FK était en
ON DELETE CASCADE, ce trigger viendrait contredire ce choix (il empêcherait la suppression même si la FK autorise le cascade).
2. Création du trigger trg_protect_village
CREATE TRIGGER trg_protect_village
BEFORE DELETE ON village
FOR EACH ROW
EXECUTE FUNCTION interdire_suppression_village();
Justification des choix :
BEFORE DELETE: on teste avant la suppression effective.FOR EACH ROW: une vérification par village supprimé.- Si la fonction lève une exception, la commande
DELETEest annulée.
3. Tests de validation
Cas 1 — village occupé (au moins un séjour)
- Vérifier :
SELECT idv, ville, capacite
FROM village
WHERE idv = 10;
SELECT *
FROM sejour
WHERE idv = 10;
- Tentative de suppression :
DELETE FROM village WHERE idv = 10;
Attendu :
ERROR: Suppression interdite : 3 séjours encore liés au village 10
SQLSTATE: P0002
Aucun DELETE effectif, les lignes de village et sejour restent présentes.
Cas 2 — village libre (aucun séjour)
- S’assurer qu’il n’y a pas de séjour :
SELECT *
FROM sejour
WHERE idv = 11;
résultat vide.
- Suppression :
DELETE FROM village WHERE idv = 11;
Attendu : suppression réussie, aucune exception, pas d’effet sur d’autres tables.
Effet global :
- Toute tentative de suppression “dangereuse” de village est bloquée, avec un diagnostic clair.
- La cohérence avec le cahier des charges est assurée au niveau base, indépendamment du code applicatif.
Correction Exercice 4 — Mise à jour automatique des totaux de séjours par client
0. Préparation du modèle
Ajouter la colonne nb_sejours dans client :
ALTER TABLE client
ADD COLUMN nb_sejours int NOT NULL DEFAULT 0;
Initialiser les valeurs à partir de l’existant :
UPDATE client c
SET nb_sejours = sub.nb
FROM (
SELECT idc, count(*) AS nb
FROM sejour
GROUP BY idc
) AS sub
WHERE c.idc = sub.idc;
-- Pour les clients sans séjour, nb_sejours reste à 0
Objectif : à partir de maintenant, maintenir nb_sejours automatiquement via triggers.
1. Fonction de trigger maj_nb_sejours_client
Version robuste qui gère INSERT, DELETE et UPDATE (changement de client pour un séjour).
CREATE OR REPLACE FUNCTION maj_nb_sejours_client()
RETURNS trigger AS $$
BEGIN
-- Insertion d'un séjour : +1 pour le client concerné
IF TG_OP = 'INSERT' THEN
UPDATE client
SET nb_sejours = nb_sejours + 1
WHERE idc = NEW.idc;
RETURN NEW;
END IF;
-- Suppression d'un séjour : -1 pour le client concerné
IF TG_OP = 'DELETE' THEN
UPDATE client
SET nb_sejours = nb_sejours - 1
WHERE idc = OLD.idc;
RETURN OLD;
END IF;
-- Mise à jour d'un séjour : cas particulier si le séjour change de client
IF TG_OP = 'UPDATE' THEN
IF NEW.idc <> OLD.idc THEN
UPDATE client
SET nb_sejours = nb_sejours - 1
WHERE idc = OLD.idc;
UPDATE client
SET nb_sejours = nb_sejours + 1
WHERE idc = NEW.idc;
END IF;
RETURN NEW;
END IF;
-- Par sécurité, branche non atteinte
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Points techniques :
TG_OPest une variable système dans un trigger :'INSERT','UPDATE','DELETE'.NEWetOLDreprésentent la ligne après / avant modification.- On ajuste
nb_sejoursuniquement pour les clients concernés. - On gère explicitement le cas UPDATE où
idcchange, sinon aucune modification.
2. Trigger sur sejour
Un seul trigger qui couvre les trois opérations :
CREATE TRIGGER trg_maj_nb_sejours_client
AFTER INSERT OR DELETE OR UPDATE OF idc ON sejour
FOR EACH ROW
EXECUTE FUNCTION maj_nb_sejours_client();
Justification :
AFTER: le séjour est déjà validé par les contraintes (FK, dates, etc.). On ne touche qu’au compteur.FOR EACH ROW: mise à jour fine pour chaque séjour.UPDATE OF idc: le trigger ne se déclenche sur UPDATE que siidcchange.
3. Jeux de tests
Insertion
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-10', '2025-07-15');
SELECT idc, nb_sejours FROM client WHERE idc = 1;
-- nb_sejours doit avoir augmenté de 1
Suppression
DELETE FROM sejour
WHERE ids = <un_ids_du_client_1>;
SELECT idc, nb_sejours FROM client WHERE idc = 1;
-- nb_sejours doit avoir diminué de 1
Changement de client
-- On suppose un séjour ids = 50, actuellement pour idc = 1
UPDATE sejour
SET idc = 2
WHERE ids = 50;
-- Vérification
SELECT idc, nb_sejours FROM client WHERE idc IN (1,2);
-- client 1 : -1
-- client 2 : +1
4. Remarque d’architecture
nb_sejoursest une dérivée d’une information déjà présente (count(*)sursejour).- On choisit de la stocker pour accélérer certains affichages (bilan client) au prix d’une logique de maintenance en trigger.
- L’exercice illustre l’usage combiné de
TG_OP,NEW,OLDet des triggersAFTERpour maintenir des totaux cohérents.
Correction Exercice 5 — Gestion des erreurs et messages
Objectif : exploiter les différents niveaux de messages (NOTICE, WARNING, EXCEPTION) et voir l’effet sur les transactions.
On part des fonctions déjà écrites aux exercices 1–3 et on les fait évoluer.
1. Exemple 1 : journalisation “soft” avec RAISE NOTICE
Sur la fonction d’archivage (Ex. 1), on peut affiner le message.
Version enrichie :
CREATE OR REPLACE FUNCTION archiver_sejour()
RETURNS trigger AS $$
BEGIN
INSERT INTO archivesejour(ids, idc, idv, debut, fin, date_archivage)
VALUES (OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin, now());
RAISE NOTICE
'Archivage : séjour % client % village % de % à %',
OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Effet :
NOTICEn’interrompt jamais la transaction.- Utile pour tracer les opérations en développement ou en audit.
Test minimal :
DELETE FROM sejour WHERE ids = 100;
-- Vérifier : message NOTICE + ligne dans archivesejour
2. Exemple 2 : avertissement non bloquant avec RAISE WARNING
Supposons que l’on veuille signaler un séjour “suspicious” mais ne pas le bloquer, par exemple un séjour très long (≥ 30 jours), tout en gardant la règle de même année.
On modifie controle_sejour_annee ainsi :
CREATE OR REPLACE FUNCTION controle_sejour_annee()
RETURNS trigger AS $$
DECLARE
v_nb_jours int;
BEGIN
IF NEW.debut IS NULL OR NEW.fin IS NULL THEN
RETURN NEW;
END IF;
-- Règle forte : même année ou erreur
IF EXTRACT(YEAR FROM NEW.debut) <> EXTRACT(YEAR FROM NEW.fin) THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Séjour invalide : début %s et fin %s ne sont pas dans la même année',
NEW.debut, NEW.fin
),
ERRCODE = 'P0001';
END IF;
-- Avertissement : séjours très longs
v_nb_jours := NEW.fin - NEW.debut;
IF v_nb_jours >= 30 THEN
RAISE WARNING
'Séjour long (% jours) pour le client % (ids potentiellement %)',
v_nb_jours, NEW.idc, NEW.ids;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
Explications :
RAISE WARNINGaffiche un message plus “sérieux” queNOTICEmais n’annule pas la commande.- Ici on accepte la ligne, mais on attire l’attention sur une situation anormale.
Tests :
-- Séjour long mais sur une seule année : accepté + WARNING
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-01', '2025-08-10');
-- Séjour multi-année : EXCEPTION, transaction annulée
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-12-28', '2026-01-05');
3. Exemple 3 : exception métier explicite avec RAISE EXCEPTION USING
Sur la fonction interdire_suppression_village (Ex. 3), on avait déjà une exception.
On la rend plus structurée :
CREATE OR REPLACE FUNCTION interdire_suppression_village()
RETURNS trigger AS $$
DECLARE
v_nb_sejours int;
BEGIN
SELECT count(*) INTO v_nb_sejours
FROM sejour
WHERE idv = OLD.idv;
IF v_nb_sejours > 0 THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Suppression interdite : % séjours encore liés au village % (%).',
v_nb_sejours, OLD.idv, OLD.ville
),
ERRCODE = 'P0002',
HINT = 'Annulez ou archivez les séjours avant de supprimer le village.';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Explications :
MESSAGE: texte principal.ERRCODE: permet de filtrer côté application.HINT: affiché par PostgreSQL après l’erreur, utile pour guider l’utilisateur.
Test :
DELETE FROM village WHERE idv = 10;
-- → ERROR avec SQLSTATE P0002, message + hint
4. Effet sur les transactions : démonstration simple
But : montrer la différence entre WARNING/NOTICE et EXCEPTION sur la persistance.
Cas 1 — WARNING, la transaction continue
BEGIN;
-- Suppose qu'on déclenche le WARNING de sejour long
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-01', '2025-08-10');
-- WARNING s'affiche, mais on peut continuer :
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-09-01', '2025-09-05');
COMMIT;
Résultat :
- Les 2 séjours sont insérés.
- Le WARNING ne provoque pas de rollback.
Cas 2 — EXCEPTION, rollback
BEGIN;
-- Séjour invalide (multi-année) → EXCEPTION dans controle_sejour_annee
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-12-28', '2026-01-05');
-- Jamais atteint si l'exception est levée
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-01', '2025-07-10');
COMMIT;
Résultat :
- Dès le premier
INSERT,RAISE EXCEPTIONannule la commande. - La transaction entière est marquée en erreur →
COMMITéchoue. - Aucun des deux séjours n’est inséré.
Conclusion :
NOTICE: journalisation innocente.WARNING: anomalie signalée, données acceptées.EXCEPTION: blocage net, rollback.
Les trois niveaux sont maintenant utilisés dans les fonctions de triggers précédentes, avec des messages métier et des codes d’erreur explicites.
Partie C
Correction Exercice 6 — Scénario complet d’intégration des triggers
But : vérifier, en un seul scénario, que tous les triggers et règles du Bloc 5 se comportent comme prévu :
- archivage automatique,
- contrôle de cohérence (même année),
- protection des villages occupés,
- gestion des messages (
NOTICE/EXCEPTION).
On suppose qu’on as déjà :
purge_sejours(p_date_limite date)(Bloc 3),trg_archive_sejour+archiver_sejour()(Ex. 1),trg_controle_sejour_annee+controle_sejour_annee()(Ex. 2),trg_protect_village+interdire_suppression_village()(Ex. 3).
1. Préparation d’un jeu de données minimal
Création d’un client, d’un village et d’un séjour valide.
BEGIN;
-- Client de test
INSERT INTO client(nom, age)
VALUES ('Client TD5', 35)
RETURNING idc;
-- Supposons que la valeur retournée soit idc = 1001
-- Village de test
INSERT INTO village(ville, activite, prix, capacite)
VALUES ('Rio', 'surf', 120, 50)
RETURNING idv;
-- Supposons idv = 2001
-- Séjour valide (même année)
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1001, 2001, '2025-07-10', '2025-07-20')
RETURNING ids;
-- Supposons ids = 3001
COMMIT;
Vérification rapide :
SELECT * FROM sejour WHERE ids = 3001;
SELECT * FROM archivesejour WHERE ids = 3001;
-- ArchiveSejour doit être encore vide pour ids=3001
2. Purge + archivage
On réutilise purge_sejours(p_date_limite date) qui fait un DELETE sur sejour.
Le trigger trg_archive_sejour doit archiver les lignes supprimées.
-- On choisit une date limite > fin du séjour (20 juillet 2025)
CALL purge_sejours('2025-12-31');
Effet attendu :
La procédure
purge_sejourssupprime les séjours dontfin < '2025-12-31'.Pour chaque ligne supprimée,
trg_archive_sejourse déclenche et :- insère la ligne correspondante dans
ArchiveSejour, - affiche un
NOTICEdu typeArchivage : séjour 3001 client 1001 village 2001 de 2025-07-10 à 2025-07-20.
- insère la ligne correspondante dans
Contrôle :
-- Doit être vide (séjour purgé)
SELECT * FROM sejour WHERE ids = 3001;
-- Doit contenir l'archive
SELECT ids, idc, idv, debut, fin, date_archivage
FROM archivesejour
WHERE ids = 3001
ORDER BY date_archivage DESC
LIMIT 1;
3. Protection de la suppression d’un village occupé
On recrée un séjour pour réutiliser le village et déclencher le trigger de protection.
BEGIN;
-- Recréer un séjour pour le village 2001
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1001, 2001, '2025-08-01', '2025-08-05')
RETURNING ids;
-- Supposons ids = 3002
-- Vérifier la présence du séjour
SELECT * FROM sejour WHERE ids = 3002;
Tentative de suppression du village :
DELETE FROM village WHERE idv = 2001;
Effet attendu :
trg_protect_village(BEFORE DELETE) s’exécute.- Il compte les séjours
sejour.idv = 2001. - Comme au moins un séjour existe (
ids = 3002), il lève :
ERROR: Suppression interdite : 1 séjours encore liés au village 2001 (Rio).
SQLSTATE: P0002
HINT: Annulez ou archivez les séjours avant de supprimer le village.
Vérification post-erreur :
-- Le village n'a PAS été supprimé
SELECT * FROM village WHERE idv = 2001;
-- Le séjour est toujours là
SELECT * FROM sejour WHERE ids = 3002;
ROLLBACK; -- on annule la transaction qui contenait le DELETE
4. Contrôle de cohérence : séjour multi-année interdit
On teste maintenant le trigger trg_controle_sejour_annee.
-- Séjour valide (même année) : doit passer
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1001, 2001, '2025-09-01', '2025-09-10');
-- Séjour invalide (multi-année) : doit échouer
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1001, 2001, '2025-12-28', '2026-01-05');
Effet attendu pour le second INSERT :
ERROR: Séjour invalide : début 2025-12-28 et fin 2026-01-05 ne sont pas dans la même année
SQLSTATE: P0001
Vérification :
-- Le séjour multi-année ne doit pas exister
SELECT *
FROM sejour
WHERE idc = 1001
AND idv = 2001
AND debut = '2025-12-28'
AND fin = '2026-01-05';
Résultat attendu : 0 ligne.
5. Synthèse du scénario
Ce scénario valide :
Archivage automatique Toute suppression de séjour via
purge_sejourscrée une entrée dansArchiveSejourgrâce au triggerAFTER DELETE ON sejour.Protection de la suppression d’un village occupé Le trigger
BEFORE DELETE ON villagecompte les séjours associés et bloque la suppression avec un message métier explicite.Contrôle de cohérence sur l’année Le trigger
BEFORE INSERT OR UPDATE ON sejourinterdit les séjours qui traversent deux années et lève uneEXCEPTIONavec unSQLSTATEspécifique.Messages Les
NOTICEetEXCEPTIONapparaissent bien dans le client SQL et traduisent les règles métier de l’application de séjours.
Correction Exercice 7 — Journalisation via log_evenement
Exercice (rappel, reformulé)
Créer un journal log_evenement alimenté par un trigger AFTER INSERT OR DELETE ON sejour.
Colonnes minimales : evenement text, horodatage timestamptz default now().
Visualiser ensuite les logs des dernières opérations.
1. Création de la table de log
Version minimale, avec un identifiant technique :
CREATE TABLE log_evenement (
id_log bigserial PRIMARY KEY,
evenement text NOT NULL,
horodatage timestamptz NOT NULL DEFAULT now()
);
Remarques :
bigserial: identifiant unique, utile pour trier les événements.evenement: texte libre, on y met un message métier.horodatage: date/heure de l’événement, par défautnow().
2. Fonction de trigger
Objectif :
- loguer les INSERT et DELETE sur
sejouravec un message lisible ; - éventuellement loguer aussi les UPDATE si on veut compléter.
CREATE OR REPLACE FUNCTION log_evenement_sejour()
RETURNS trigger AS $$
DECLARE
v_msg text;
BEGIN
IF TG_OP = 'INSERT' THEN
v_msg := format(
'INSERT sejour: ids=%s, idc=%s, idv=%s, debut=%s, fin=%s',
NEW.ids, NEW.idc, NEW.idv, NEW.debut, NEW.fin
);
INSERT INTO log_evenement(evenement)
VALUES (v_msg);
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
v_msg := format(
'DELETE sejour: ids=%s, idc=%s, idv=%s, debut=%s, fin=%s',
OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin
);
INSERT INTO log_evenement(evenement)
VALUES (v_msg);
RETURN OLD;
ELSE
-- Si un jour on ajoute UPDATE à ce trigger, traiter ici
RETURN NULL;
END IF;
END;
$$ LANGUAGE plpgsql;
Points techniques :
TG_OPindique l’opération (INSERT,UPDATE,DELETE).- On utilise
NEWpourINSERT,OLDpourDELETE. format()permet de construire un message structuré.- On ne fixe pas
horodatage: la valeur par défautnow()joue son rôle.
3. Création du trigger
CREATE TRIGGER trg_log_evenement_sejour
AFTER INSERT OR DELETE ON sejour
FOR EACH ROW
EXECUTE FUNCTION log_evenement_sejour();
Justification :
AFTER: on ne logue que si l’opération a effectivement réussi.FOR EACH ROW: un log par séjour inséré ou supprimé.
4. Jeux de tests
Insertion
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (1, 1, '2025-07-10', '2025-07-15')
RETURNING ids;
-- Supposons ids = 4001
Suppression
DELETE FROM sejour WHERE ids = 4001;
Inspecter les logs :
SELECT id_log, horodatage, evenement
FROM log_evenement
ORDER BY horodatage DESC
LIMIT 10;
Résultat typique :
id_log | horodatage | evenement
--------+------------------------------+----------------------------------------------------
2 | 2025-07-10 15:02:33.123+02 | DELETE sejour: ids=4001, idc=1, idv=1, ...
1 | 2025-07-10 15:02:32.987+02 | INSERT sejour: ids=4001, idc=1, idv=1, ...
5. Interaction avec les autres triggers
Si la suppression déclenche aussi
archiver_sejour(), l’ordre d’exécution par défaut est celui de création des triggers.En pratique, pour ce TD, l’ordre n’est pas critique :
trg_archive_sejourgère l’INSERT dansArchiveSejour,trg_log_evenement_sejourjournalise leDELETE.
On obtient donc :
- archive métier dans
ArchiveSejour; - trace générique dans
log_evenement.
Partie D
Correction Exercice 8 — Schéma dédié + vue avec trigger INSTEAD OF
Je restructure cet exo pour simplifier :
- Regrouper les fonctions de triggers dans un schéma dédié.
- Créer une vue de synthèse
vue_sejours_actifs. - Ajouter un trigger
INSTEAD OFdessus pour gérer les suppressions via la vue. - Comparer “logique en trigger” vs “logique en procédure”.
1. Schéma dédié pour la logique technique
Objectif : éviter de polluer public avec toutes les fonctions techniques (triggers, utilitaires).
On crée un schéma systeme et on y met les fonctions de triggers.
1.1. Création du schéma
CREATE SCHEMA IF NOT EXISTS systeme;
1.2. Déplacement logique des fonctions
Idée : recréer les fonctions dans le schéma systeme, puis mettre à jour les triggers.
Exemple pour archiver_sejour :
CREATE OR REPLACE FUNCTION systeme.archiver_sejour()
RETURNS trigger AS $$
BEGIN
INSERT INTO archivesejour(ids, idc, idv, debut, fin, date_archivage)
VALUES (OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin, now());
RAISE NOTICE
'Archivage : séjour % client % village % de % à %',
OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Même chose pour les autres fonctions :
CREATE OR REPLACE FUNCTION systeme.controle_sejour_annee()
RETURNS trigger AS $$
DECLARE
v_nb_jours int;
BEGIN
IF NEW.debut IS NULL OR NEW.fin IS NULL THEN
RETURN NEW;
END IF;
IF EXTRACT(YEAR FROM NEW.debut) <> EXTRACT(YEAR FROM NEW.fin) THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Séjour invalide : début %s et fin %s ne sont pas dans la même année',
NEW.debut, NEW.fin
),
ERRCODE = 'P0001';
END IF;
v_nb_jours := NEW.fin - NEW.debut;
IF v_nb_jours >= 30 THEN
RAISE WARNING
'Séjour long (% jours) pour le client % (ids potentiellement %)',
v_nb_jours, NEW.idc, NEW.ids;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION systeme.interdire_suppression_village()
RETURNS trigger AS $$
DECLARE
v_nb_sejours int;
BEGIN
SELECT count(*) INTO v_nb_sejours
FROM sejour
WHERE idv = OLD.idv;
IF v_nb_sejours > 0 THEN
RAISE EXCEPTION
USING MESSAGE = format(
'Suppression interdite : % séjours encore liés au village % (%).',
v_nb_sejours, OLD.idv, OLD.ville
),
ERRCODE = 'P0002',
HINT = 'Annulez ou archivez les séjours avant de supprimer le village.';
END IF;
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE FUNCTION systeme.maj_nb_sejours_client()
RETURNS trigger AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
UPDATE client
SET nb_sejours = nb_sejours + 1
WHERE idc = NEW.idc;
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
UPDATE client
SET nb_sejours = nb_sejours - 1
WHERE idc = OLD.idc;
RETURN OLD;
ELSIF TG_OP = 'UPDATE' THEN
IF NEW.idc <> OLD.idc THEN
UPDATE client
SET nb_sejours = nb_sejours - 1
WHERE idc = OLD.idc;
UPDATE client
SET nb_sejours = nb_sejours + 1
WHERE idc = NEW.idc;
END IF;
RETURN NEW;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
Idem pour log_evenement_sejour si on veut la déplacer :
CREATE OR REPLACE FUNCTION systeme.log_evenement_sejour()
RETURNS trigger AS $$
DECLARE
v_msg text;
BEGIN
IF TG_OP = 'INSERT' THEN
v_msg := format(
'INSERT sejour: ids=%s, idc=%s, idv=%s, debut=%s, fin=%s',
NEW.ids, NEW.idc, NEW.idv, NEW.debut, NEW.fin
);
INSERT INTO log_evenement(evenement) VALUES (v_msg);
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
v_msg := format(
'DELETE sejour: ids=%s, idc=%s, idv=%s, debut=%s, fin=%s',
OLD.ids, OLD.idc, OLD.idv, OLD.debut, OLD.fin
);
INSERT INTO log_evenement(evenement) VALUES (v_msg);
RETURN OLD;
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
1.3. Mise à jour des triggers
Il faut redéfinir les triggers pour appeler les versions qualifiées :
DROP TRIGGER IF EXISTS trg_archive_sejour ON sejour;
CREATE TRIGGER trg_archive_sejour
AFTER DELETE ON sejour
FOR EACH ROW
EXECUTE FUNCTION systeme.archiver_sejour();
DROP TRIGGER IF EXISTS trg_controle_sejour_annee ON sejour;
CREATE TRIGGER trg_controle_sejour_annee
BEFORE INSERT OR UPDATE ON sejour
FOR EACH ROW
EXECUTE FUNCTION systeme.controle_sejour_annee();
DROP TRIGGER IF EXISTS trg_protect_village ON village;
CREATE TRIGGER trg_protect_village
BEFORE DELETE ON village
FOR EACH ROW
EXECUTE FUNCTION systeme.interdire_suppression_village();
DROP TRIGGER IF EXISTS trg_maj_nb_sejours_client ON sejour;
CREATE TRIGGER trg_maj_nb_sejours_client
AFTER INSERT OR DELETE OR UPDATE OF idc ON sejour
FOR EACH ROW
EXECUTE FUNCTION systeme.maj_nb_sejours_client();
DROP TRIGGER IF EXISTS trg_log_evenement_sejour ON sejour;
CREATE TRIGGER trg_log_evenement_sejour
AFTER INSERT OR DELETE ON sejour
FOR EACH ROW
EXECUTE FUNCTION systeme.log_evenement_sejour();
Intérêt :
- séparation claire modèle métier (
public.client,public.sejour, …) / plomberie technique (systeme.*), - lecture plus propre du schéma public.
2. Vue de synthèse des séjours actifs
Objectif : fournir une vue “orientée métier” pour lecture, mais aussi pour permettre une suppression logique via la vue.
2.1. Création de la vue
Séjours actifs à la date du jour (ou on fixe une condition plus simple).
CREATE OR REPLACE VIEW vue_sejours_actifs AS
SELECT
s.ids,
s.idc,
c.nom AS nom_client,
s.idv,
v.ville,
v.activite,
s.debut,
s.fin
FROM sejour s
JOIN client c USING(idc)
JOIN village v USING(idv)
WHERE CURRENT_DATE BETWEEN s.debut AND s.fin;
Vue simple, lecture seule par défaut.
Objectif du trigger INSTEAD OF : autoriser :
DELETE FROM vue_sejours_actifs WHERE ids = ...;
en le réinterprétant sur la table sejour (avec archivage automatique déjà en place).
3. Trigger INSTEAD OF sur la vue
3.1. Fonction de trigger
CREATE OR REPLACE FUNCTION systeme.delete_sejour_via_vue()
RETURNS trigger AS $$
BEGIN
-- On redirige la suppression vers la table de base
DELETE FROM sejour
WHERE ids = OLD.ids;
-- Le trigger INSTEAD OF doit renvoyer OLD pour DELETE
RETURN OLD;
END;
$$ LANGUAGE plpgsql;
Remarque :
- On n’essaie pas de supprimer les lignes dans
clientouvillage: ce n’est pas l’objectif métier. - Le trigger
AFTER DELETE ON sejour(trg_archive_sejour) s’exécutera et archivera automatiquement le séjour.
3.2. Définition du trigger INSTEAD OF
CREATE TRIGGER trg_delete_sejour_via_vue
INSTEAD OF DELETE ON vue_sejours_actifs
FOR EACH ROW
EXECUTE FUNCTION systeme.delete_sejour_via_vue();
Points clés :
INSTEAD OFn’est possible que sur des vues.- PostgreSQL exécute cette fonction à la place de la tentative de
DELETEsur la vue.
4. Tests de bout en bout
4.1. Insertion d’un séjour actif
INSERT INTO client(nom, age) VALUES ('Client Vue', 40) RETURNING idc;
-- supposons idc=5001
INSERT INTO village(ville, activite, prix, capacite)
VALUES ('Nice', 'plage', 100, 20) RETURNING idv;
-- supposons idv=6001
INSERT INTO sejour(idc, idv, debut, fin)
VALUES (5001, 6001, CURRENT_DATE, CURRENT_DATE + 5)
RETURNING ids;
-- supposons ids=7001
Vérifier la vue :
SELECT * FROM vue_sejours_actifs WHERE ids = 7001;
Doit retourner 1 ligne.
4.2. Suppression via la vue
DELETE FROM vue_sejours_actifs WHERE ids = 7001;
Effet prévu :
Le
INSTEAD OFappellesysteme.delete_sejour_via_vue():DELETE FROM sejour WHERE ids = 7001;
Ce
DELETEdéclenche :trg_archive_sejour→ insertion dansarchivesejour,trg_log_evenement_sejour→ log danslog_evenement,trg_maj_nb_sejours_client→ mise à jour denb_sejours, etc.
Contrôles :
-- Le séjour a disparu de la table
SELECT * FROM sejour WHERE ids = 7001;
-- Il n'est plus visible dans la vue
SELECT * FROM vue_sejours_actifs WHERE ids = 7001;
-- Il a été archivé
SELECT * FROM archivesejour WHERE ids = 7001 ORDER BY date_archivage DESC LIMIT 1;
-- Il est logué
SELECT * FROM log_evenement
WHERE evenement LIKE 'DELETE sejour: ids=7001%'
ORDER BY horodatage DESC
LIMIT 1;
5. Comparaison : triggers vs procédures
Ce qui émerge de cet exercice :
Procédures (Bloc 3–4)
- utilisées par le code applicatif de manière explicite (
CALL purge_sejours(...),CALL annuler_sejour(...)), - logique claire, mais rien n’empêche d’agir directement sur les tables en contournant la procédure.
- utilisées par le code applicatif de manière explicite (
Triggers (Bloc 5)
- s’appliquent à toutes les opérations sur les tables / vues concernées,
- imposent les règles métier même si un script SQL “brut” est exécuté,
- centralisent des tâches techniques : archivage, comptages, journaux.
Vue + trigger
INSTEAD OF- offrent une API SQL plus riche : on manipule
vue_sejours_actifscomme un objet métier, - la logique d’écriture est routée vers les bonnes tables et déclencheurs.
- offrent une API SQL plus riche : on manipule
Conclusion technique :
- Procédures = entrée “officielle” pour l’application.
- Triggers = garde-fous et automatisation sous-jacente.
- Vues + triggers
INSTEAD OF= façade métier SQL au-dessus de la structure physique.