08 - Les triggers : reagir automatiquement aux changements
Ce que tu vas apprendre
- Creer des trigger functions et les associer a des tables
- BEFORE vs AFTER, ROW vs STATEMENT
- NEW, OLD, TG_OP, TG_TABLE_NAME
- Cas pratiques : updated_at, audit log, validation, soft delete
Prerequisites
Avoir lu l'article 07 sur les fonctions PL/pgSQL.
Qu'est-ce qu'un trigger
Un trigger est une fonction qui s'exécuté automatiquement quand une table est modifiee (INSERT, UPDATE, DELETE). Tu ne l'appelles pas toi-meme, PostgreSQL le fait pour toi.
C'est en deux parties : la trigger function (le code) et le trigger (la liaison entre la fonction et la table).
Le trigger le plus courant : updated_at
Quasiment toutes les applications ont besoin d'un updated_at qui se met à jour automatiquement :
sql-- 1. La trigger function
CREATE OR REPLACE FUNCTION update_timestamp()
RETURNS TRIGGER AS $
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$ LANGUAGE plpgsql;
-- 2. Associer le trigger a une table
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
customer_id INT NOT NULL,
total NUMERIC(10,2) NOT NULL,
status TEXT DEFAULT 'pending',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE TRIGGER set_updated_at
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION update_timestamp();
Test :
sqlINSERT INTO orders (customer_id, total) VALUES (1, 150.00);
SELECT id, status, updated_at FROM orders;
-- updated_at = 2026-03-28 10:00:00 (par exemple)
-- 5 minutes plus tard...
UPDATE orders SET status = 'completed' WHERE id = 1;
SELECT id, status, updated_at FROM orders;
-- updated_at = 2026-03-28 10:05:00 (mis a jour automatiquement)
La meme trigger function peut etre reutilisee sur toutes tes tables. C'est ce qu'on fait en général.
NEW vs OLD
Dans une trigger function :
- NEW : la ligne apres la modification (disponible en INSERT et UPDATE)
- OLD : la ligne avant la modification (disponible en UPDATE et DELETE)
| Opération | NEW | OLD |
|---|---|---|
| INSERT | oui | non |
| UPDATE | oui | oui |
| DELETE | non | oui |
BEFORE vs AFTER
- BEFORE : le trigger s'exécuté avant que la modification soit appliquee. Tu peux modifier NEW (changer des valeurs avant l'INSERT/UPDATE). Si tu retournes NULL, l'opération est annulee.
- AFTER : le trigger s'exécuté apres la modification. La ligne est deja écrite. Utile pour les effets de bord (logs, notifications, mises à jour d'autres tables).
sql-- BEFORE : on peut modifier les donnees
CREATE OR REPLACE FUNCTION normalize_email()
RETURNS TRIGGER AS $
BEGIN
NEW.email = LOWER(TRIM(NEW.email));
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER normalize_email_trigger
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION normalize_email();
ROW vs STATEMENT
- FOR EACH ROW : le trigger s'exécuté une fois par ligne affectee. Si tu fais
UPDATE orders SET status = 'cancelled' WHERE customer_id = 5et que ca touche 10 lignes, le trigger s'exécuté 10 fois. - FOR EACH STATEMENT : le trigger s'exécuté une seule fois pour l'instruction entière, peu importe le nombre de lignes. Utile pour les notifications ou les logs agrages.
En pratique, FOR EACH ROW est de loin le plus utilise.
L'audit log complet
Un vrai trigger d'audit qui enregistre toutes les modifications :
sqlCREATE TABLE audit_log (
id SERIAL PRIMARY KEY,
table_name TEXT NOT NULL,
operation TEXT NOT NULL,
old_data JSONB,
new_data JSONB,
changed_at TIMESTAMP DEFAULT NOW(),
changed_by TEXT DEFAULT current_user
);
CREATE OR REPLACE FUNCTION audit_changes()
RETURNS TRIGGER AS $
BEGIN
INSERT INTO audit_log (table_name, operation, old_data, new_data)
VALUES (
TG_TABLE_NAME,
TG_OP,
CASE WHEN TG_OP IN ('UPDATE', 'DELETE') THEN row_to_json(OLD)::JSONB ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW)::JSONB ELSE NULL END
);
RETURN COALESCE(NEW, OLD);
END;
$ LANGUAGE plpgsql;
Associer le trigger a une ou plusieurs tables :
sqlCREATE TRIGGER audit_orders
AFTER INSERT OR UPDATE OR DELETE ON orders
FOR EACH ROW
EXECUTE FUNCTION audit_changes();
CREATE TRIGGER audit_users
AFTER INSERT OR UPDATE OR DELETE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_changes();
Les variables speciales du trigger :
TG_TABLE_NAME: le nom de la tableTG_OP: l'opération ('INSERT', 'UPDATE', 'DELETE')TG_WHEN: 'BEFORE' ou 'AFTER'TG_LEVEL: 'ROW' ou 'STATEMENT'
row_to_json(OLD) convertit la ligne entière en JSON. C'est hyper pratique pour l'audit parce que tu n'as pas besoin de lister chaque colonne.
Trigger de validation
Un BEFORE trigger qui empeche certaines modifications :
sqlCREATE OR REPLACE FUNCTION prevent_completed_order_changes()
RETURNS TRIGGER AS $
BEGIN
IF OLD.status = 'completed' AND NEW.status != OLD.status THEN
RAISE EXCEPTION 'Impossible de modifier le statut d''une commande completee (order_id=%)', OLD.id;
END IF;
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER protect_completed_orders
BEFORE UPDATE ON orders
FOR EACH ROW
EXECUTE FUNCTION prevent_completed_order_changes();
sqlUPDATE orders SET status = 'cancelled' WHERE id = 1;
-- ERROR: Impossible de modifier le statut d'une commande completee (order_id=1)
C'est une couche de sécurité supplementaire. Meme si un bug dans l'application essaie de modifier une commande completee, la base refuse. En architecture hexagonale, ce genre de regle existe aussi dans le domaine, mais le trigger est une deuxieme ligne de defense.
Soft delete avec trigger
Au lieu de supprimer les lignes, on les marque comme supprimees :
sqlCREATE OR REPLACE FUNCTION soft_delete()
RETURNS TRIGGER AS $
BEGIN
-- Au lieu de supprimer, on met a jour deleted_at
UPDATE orders SET deleted_at = NOW() WHERE id = OLD.id;
RETURN NULL; -- NULL = annule le DELETE reel
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER orders_soft_delete
BEFORE DELETE ON orders
FOR EACH ROW
EXECUTE FUNCTION soft_delete();
RETURN NULL dans un BEFORE trigger annule l'opération. Le DELETE est transforme en UPDATE. L'application n'a rien a changer dans son code.
Trigger conditionnel avec WHEN
sqlCREATE TRIGGER log_status_changes
AFTER UPDATE ON orders
FOR EACH ROW
WHEN (OLD.status IS DISTINCT FROM NEW.status)
EXECUTE FUNCTION audit_changes();
Le trigger ne s'exécuté que quand le statut change vraiment. IS DISTINCT FROM gere aussi les NULL (contrairement a !=).
INSTEAD OF sur les vues
Tu peux rendre une vue modifiable avec un trigger INSTEAD OF :
sqlCREATE VIEW active_orders AS
SELECT * FROM orders WHERE deleted_at IS NULL;
CREATE OR REPLACE FUNCTION handle_active_orders_insert()
RETURNS TRIGGER AS $
BEGIN
INSERT INTO orders (customer_id, total, status)
VALUES (NEW.customer_id, NEW.total, NEW.status);
RETURN NEW;
END;
$ LANGUAGE plpgsql;
CREATE TRIGGER active_orders_insert
INSTEAD OF INSERT ON active_orders
FOR EACH ROW
EXECUTE FUNCTION handle_active_orders_insert();
Désactiver/activer un trigger
sql-- Desactiver un trigger specifique
ALTER TABLE orders DISABLE TRIGGER audit_orders;
-- Desactiver tous les triggers d'une table
ALTER TABLE orders DISABLE TRIGGER ALL;
-- Reactiver
ALTER TABLE orders ENABLE TRIGGER audit_orders;
ALTER TABLE orders ENABLE TRIGGER ALL;
Utile pendant un import de donnees en masse pour éviter de remplir l'audit log avec des millions de lignes.
Les dangers des triggers
J'utilise les triggers pour trois choses : updated_at, audit, et les contraintes de coherence impossibles a exprimer avec un CHECK. Pour le reste, la logique reste dans l'application.
Pourquoi ? Parce que les triggers sont invisibles. Un dev qui lit le code de l'application ne voit pas qu'un INSERT dans orders déclenché 3 triggers qui inserent dans d'autres tables, envoient une notification et mettent à jour un compteur. C'est du code cache, difficile a debugger, impossible a tester unitairement.
Les triggers ont aussi un impact sur les performances. Chaque INSERT/UPDATE/DELETE active les triggers, meme les opérations de maintenance. Sur des tables a fort volume d'écriture, un trigger mal optimise peut devenir un goulot d'etranglement.
La regle que je suis : si le trigger fait plus de 5 lignes de logique, ca devrait probablement etre dans l'application.
Sur paltemps.fr, les triggers sont limites a updated_at et audit_log. Toute la logique métier passe par les use cases de l'architecture hexagonale.
Résumé
- Les triggers executent du code automatiquement sur INSERT/UPDATE/DELETE
- BEFORE pour modifier ou annuler, AFTER pour les effets de bord
- NEW (apres) et OLD (avant) donnent acces aux donnees de la ligne
- Cas d'usage principaux : updated_at, audit log, validation, soft delete
- Les triggers sont puissants mais invisibles : limiter leur usage a l'infra, pas a la logique métier
Article précédent : 07 - Fonctions et stored procedures Article suivant : 09 - Vues et vues materialisees