Le carnet
Chapitre 5 · encre sienne

PL/SQL — triggers & séquences

On déporte les traitements vers le SGBD. Variables typées, blocs structurés, déclencheurs sur événements et compteurs auto-incrémentés.

~ 35 min de lecture TP5 — Producteur de vin 14 flashcards

1. PL/SQL — généralités

  • Déporter les traitements de manipulation de données vers le SGBD.
  • Stockage du code sous forme de :
    • triggers (réactifs aux événements)
    • fonctions / procédures (appelables)
    • packages (regroupements)
  • Langage propre : PL/SQL — Procedural Language for SQL.
  • Fournit un ensemble de fonctionnalités communes aux applicatifs.
Pourquoi PL/SQL plutôt qu'un script client ? Le code vit dans la base. Pas de réseau pour chaque ordre SQL. Tous les clients (Java, Python, app maison) partagent la même logique. Et tu peux poser des garde-fous qui s'appliquent même si quelqu'un bidouille à la main.

Structure générale d'un bloc

DECLARE déclarations de variables, curseurs, types… BEGIN corps du traitement SELECT · INSERT · UPDATE · IF · LOOP · … EXCEPTION WHEN err1 THEN … · WHEN OTHERS THEN … END;

Un bloc PL/SQL est une descente structurée : déclarations, code, gestion d'erreurs, fin.

DECLARE
   <bloc de variables>
BEGIN
   <bloc de traitements>
EXCEPTION
   WHEN err1 THEN ...
   WHEN err2 THEN ...
   WHEN OTHERS THEN ...
END;

Sortie standard — pour debug

  • Package DBMS_OUTPUT · procédure PUT_LINE(...)
  • Activation côté session : SET SERVEROUTPUT ON
DBMS_OUTPUT.PUT_LINE('Coucou ' || TO_CHAR(12));

2. Variables et affectations

Trois familles de variables :

  • scalaires : nombres, chaînes, date…
  • composées : RECORD, TABLE, VARRAY
  • curseurs (voir CM6)
-- Type prédéfini
v_prix NUMBER(6,2);

-- Type déduit d'une colonne
v_nom client.nom%TYPE;
💡 %TYPE et %ROWTYPE
Au lieu d'écrire VARCHAR2(50) et risquer d'être désynchronisé avec la table, écris table.col%TYPE ou table%ROWTYPE. Si la table change, le code suit automatiquement.

Affectation — trois formes

-- ① Classique avec :=
v_tva := 19.6;

-- ② Sur requête avec INTO
v_etudiant etudiant%ROWTYPE;
...
SELECT * INTO v_etudiant FROM etudiant WHERE id = 42;

-- ③ Sur curseur avec FETCH (cf CM6)
FETCH mon_curseur INTO v_ligne;

3. Types composés

Type RECORD — comme une struct C

-- Définir le type
TYPE t_rec_eleve IS RECORD (
   nom    VARCHAR2(50),
   prenom VARCHAR2(30),
   age    NUMBER(3)
);

-- Déclarer la variable
v_eleve t_rec_eleve;

-- Variante : déduit d'une table
v_adresse adresse%ROWTYPE;

Types tableau — TABLE vs VARRAY

TYPECaractéristiquesQuand l'utiliser
TABLETableau associatif (index par clé entière)Taille non connue, accès par clé arbitraire
VARRAYTableau classique à taille variable mais bornéeTaille max connue, parcours séquentiel 1..N
  • Une seule dimension. Mais possibilité de TABLE de TABLE ou VARRAY de VARRAY.
  • Choix selon : mode de parcours + connaissance de la taille.
-- TABLE associative
TYPE tab_noms IS TABLE OF eleve.nom%TYPE
   INDEX BY PLS_INTEGER;

-- VARRAY borné à 7 éléments
TYPE tab_jours IS VARRAY(7) OF VARCHAR2(8);

v_noms  tab_noms;
v_jours tab_jours;

-- Affectations
v_noms(3308) := 'toto';
v_noms(6712) := 'titi';

v_jours := tab_jours('lundi', 'mardi', 'mercredi',
   'jeudi', 'vendredi', 'samedi', 'dimanche');
🗂️ Différence clé d'index
TABLE : index par PLS_INTEGER quelconque (3308, 6712, …) → sparse.
VARRAY : indices contigus 1..N → dense, comme un array C.

4. Instructions de contrôle

Conditionnelle

IF cond1 THEN
   trt1;
ELSIF cond2 THEN
   trt2;
ELSE
   trt3;
END IF;

Trois sortes de boucles

-- ① LOOP avec EXIT WHEN
LOOP
   trt;
   EXIT WHEN condition;
END LOOP;

-- ② FOR avec borne
FOR v_it IN deb..fin LOOP
   trt;
END LOOP;

-- ③ WHILE
WHILE condition LOOP
   trt;
END LOOP;

Parcours de tableau associatif

-- Variante FIRST / NEXT (résistant aux trous d'index)
BEGIN
   i := v_noms.FIRST;
   WHILE i IS NOT NULL LOOP
      DBMS_OUTPUT.PUT_LINE('Élève ' || v_noms(i) || ', ID : ' || TO_CHAR(i));
      i := v_noms.NEXT(i);
   END LOOP;
END;

-- Variante FOR i IN FIRST..LAST
FOR i IN v_noms.FIRST..v_noms.LAST LOOP
   DBMS_OUTPUT.PUT_LINE(v_noms(i));
END LOOP;

Parcours de tableau classique (VARRAY)

  • Premier élément : indice 1
  • Taille : v_tab.COUNT
FOR i IN 1..v_jours.COUNT LOOP
   DBMS_OUTPUT.PUT_LINE(v_jours(i));
END LOOP;

5. Triggers

Définition Un trigger est du code PL/SQL stocké en base qui se déclenche lors d'un événement sur : une TABLE, une VUE, ou un SCHÉMA/DATABASE.

Instructions de déclenchement

FamilleÉvénements
LDDCREATE, ALTER, DROP
LMDINSERT, DELETE, UPDATE
DATABASESTARTUP, SHUTDOWN, LOGON, …

Moment de déclenchement autour de l'événement : BEFORE ou AFTER.

trigger BEFORE avant l'événement INSERT / UPDATE / DELETE trigger AFTER après l'événement BEFORE = pour intercepter/modifier · AFTER = pour réagir/propager

Le trigger BEFORE peut modifier les valeurs (:new) ou refuser l'événement. Le trigger AFTER agit après confirmation.

💡 Penser garde-fou Il n'y a pas que les applicatifs qui modifient les données — DBA, scripts admin, autres clients. Les triggers garde-fous dans la base s'appliquent à tout le monde.

Syntaxe trigger sur table

CREATE [OR REPLACE] TRIGGER nom_trig
BEFORE|AFTER INSERT|DELETE|UPDATE ON nom_table
[FOR EACH ROW]
[WHEN condition]
DECLARE
   variables
BEGIN
   instructions PL/SQL
END;
👯 :new et :old
Disponibles avec FOR EACH ROW. :new = nouvelle valeur, :old = ancienne valeur (utile pour UPDATE/DELETE).
⚠️ Table en mutation
Lors de l'exécution d'un trigger, on ne requête pas la table en cours de mutation (a fortiori, on n'écrit pas la requête en cours d'exécution).

Plusieurs déclenchements / colonne particulière

-- Plusieurs événements dans un seul trigger
CREATE OR REPLACE TRIGGER nom_trig
BEFORE INSERT OR DELETE ON nom_table
BEGIN
   IF INSERTING THEN
      ...
   ELSIF DELETING THEN
      ...
   END IF;
END;

-- Sur colonnes particulières (UPDATE uniquement)
CREATE OR REPLACE TRIGGER nom_trig
BEFORE UPDATE OF col1, col2 ON nom_table
DECLARE ...
BEGIN ...
END;

États et gestion du trigger

-- Activer / désactiver
ALTER TRIGGER nom_trig {ENABLE|DISABLE};

-- Recompiler après modification
ALTER TRIGGER nom_trig COMPILE;

-- Supprimer
DROP TRIGGER nom_trig;

Exemple : mise à jour de stock après commande

CREATE OR REPLACE TRIGGER trig_maj_stock
AFTER INSERT ON ligne_commande
FOR EACH ROW
BEGIN
   UPDATE PRODUIT
      SET nb_stock = nb_stock - :new.quantite
      WHERE id_produit = :new.id_produit;
END;

Exemple garde-fou — refus d'une donnée invalide

CREATE OR REPLACE TRIGGER trig_stock_positif
BEFORE UPDATE OF nb_stock ON produit
FOR EACH ROW
BEGIN
   IF :new.nb_stock < 0 THEN
      RAISE_APPLICATION_ERROR(-20012, 'Mise à jour du stock impossible (<0)');
   END IF;
END;
⚠️ Codes RAISE_APPLICATION_ERROR Code strictement inférieur à -20000 obligatoire. Plage réservée par Oracle : -20000 à -20999.

Triggers sur vue — INSTEAD OF

Une vue accepte des modifications de données uniquement si elle contient des clés/champs uniques, pas de regroupement ni de sous-requête. Pour les vues non modifiables, on utilise un trigger INSTEAD OF qui remplace le comportement par défaut.

CREATE [OR REPLACE] TRIGGER nom_trig
INSTEAD OF INSERT ON nom_vue
[DECLARE]
   ...
BEGIN
   traitement alternatif
END;

Exemple complet — vue jointure film + réalisateur

CREATE OR REPLACE VIEW vue_film_real AS
   SELECT num_film, titre, annee, f.num_ind AS id_real, nom, prenom
   FROM film f, individu i
   WHERE f.NUM_IND = i.NUM_IND;

CREATE OR REPLACE TRIGGER trig_ins_vue_film_real
INSTEAD OF INSERT ON vue_film_real
DECLARE
   v_id_real INDIVIDU.NUM_IND%TYPE;
BEGIN
   BEGIN
      SELECT num_ind INTO v_id_real
      FROM INDIVIDU
      WHERE num_ind = :new.id_real;
   EXCEPTION
      WHEN NO_DATA_FOUND THEN
         INSERT INTO individu VALUES (seq_id_real.NEXTVAL, :new.nom, :new.prenom)
            RETURNING num_ind INTO v_id_real;
   END;

   INSERT INTO film (num_film, titre, annee, num_ind)
   VALUES (seq_id_film.NEXTVAL, :new.titre, :new.annee, v_id_real);
END;
🎯 Pattern d'examen
Le trigger d'INSTEAD OF qui « éclate » un INSERT sur une vue de jointure vers les deux tables sources — c'est la question piège type partiel.

6. Séquences

  • Permettent de générer automatiquement des valeurs entières (typiquement des clés).
  • Le déclenchement d'un trigger associé à une table permet la gestion d'auto-incrémentation.
  • Une séquence peut être incrémentée et interrogée.

Syntaxe

CREATE SEQUENCE nom_seq
[INCREMENT BY n]      -- défaut : 1
[START WITH m]        -- défaut : 1
[MINVALUE k]
[MAXVALUE p]
[CYCLE | NOCYCLE]
[CACHE l | NOCACHE];

Accesseurs

AccesseurEffet
nom_seq.NEXTVALGénère le nombre suivant (incrémente)
nom_seq.CURRVALRécupère la valeur en cours (sans incrémenter)
INSERT INTO users VALUES (seq_id_usr.NEXTVAL, 'toto');
SELECT seq_id_usr.CURRVAL INTO valeurID FROM DUAL;

Pattern canonique — auto-incrément par trigger

CREATE OR REPLACE TRIGGER id_eleve
BEFORE INSERT ON eleve
FOR EACH ROW
DECLARE
   v_user VARCHAR2(10);
BEGIN
   -- nom de l'utilisateur connecté
   SELECT USER INTO v_user FROM DUAL;

   :new.id_eleve  := seq_id_eleve.NEXTVAL;
   :new.date_ajout := SYSDATE;
   :new.cree_par  := v_user;
END;
INSERT INTO eleve VALUES (NULL, 'Bob', 'Marley') Trigger BEFORE INSERT :new.id := seq.NEXTVAL :new.date := SYSDATE Séquence seq_id_eleve 42 → 43 → 44 → … Table eleve (43, 'Bob', 'Marley', SYSDATE, 'alice') L'app n'a pas besoin de connaître les IDs — la base les attribue elle-même

Auto-incrémentation transparente : l'application insère sans ID, le trigger l'attribue via la séquence.

Réviser le chapitre

Pour vérifier ta compréhension

Pourquoi utiliser %TYPE plutôt que d'écrire le type en dur ?

Pour suivre l'évolution du schéma. Si la colonne client.nom passe de VARCHAR2(50) à VARCHAR2(100), ton code qui utilise client.nom%TYPE s'adapte automatiquement à la recompilation. Sans %TYPE, tu aurais des erreurs de troncature ou de débordement.

Quelle est la différence entre BEFORE et AFTER trigger ?

BEFORE : exécuté avant que l'ordre LMD ne soit appliqué. C'est le seul moment où tu peux modifier :new ou lever une erreur pour empêcher l'événement. Idéal pour les garde-fous et l'auto-incrément.
AFTER : exécuté après que l'événement a réussi. Idéal pour propager une mise à jour vers d'autres tables, journaliser, etc.

Pourquoi RAISE_APPLICATION_ERROR(-20012, ...) et pas -12 ou -50000 ?

Oracle réserve la plage de codes -20000 à -20999 aux erreurs applicatives utilisateur. Hors de cette plage, c'est interdit : le code doit être strictement inférieur à -20000 et au moins égal à -20999.

Quand utilise-t-on un trigger INSTEAD OF ?

Sur une vue non directement modifiable (jointure, GROUP BY, sous-requête). Le trigger remplace le comportement par défaut : au lieu d'essayer d'écrire dans la vue, Oracle exécute le code PL/SQL qui sait comment ventiler l'insert/update/delete vers les tables sources sous-jacentes.

Pourquoi combiner trigger et séquence pour gérer une clé primaire ?

Parce que l'application ne peut pas connaître à l'avance les ID disponibles, et qu'il faut éviter les collisions entre clients concurrents. La séquence garantit l'unicité ; le trigger BEFORE INSERT FOR EACH ROW appelle NEXTVAL automatiquement à chaque insert. L'app insère sans préciser l'ID, et la base se débrouille.

🃏 Flashcards éclair

Clique pour retourner.

Bloc PL/SQL

Les 4 sections.

tourne →
DECLARE
BEGIN
EXCEPTION
END;

%TYPE / %ROWTYPE

Différence.

tourne →
tbl.col%TYPE = même type qu'une colonne
tbl%ROWTYPE = enregistrement complet d'une ligne

TABLE vs VARRAY

Différence clé.

tourne →
TABLE : tableau associatif (index PLS_INTEGER sparse)
VARRAY : tableau classique 1..N, taille bornée

3 boucles PL/SQL

Nomme-les.

tourne →
LOOP … EXIT WHEN … END LOOP
FOR i IN deb..fin LOOP
WHILE cond LOOP

Trigger syntaxe

Squelette de base.

tourne →
CREATE OR REPLACE TRIGGER nom
BEFORE|AFTER INSERT ON tbl
FOR EACH ROW
BEGINEND;

:new et :old

Quand sont-elles disponibles ?

tourne →
Uniquement avec FOR EACH ROW.
:new = nouvelle valeur, :old = ancienne valeur.

BEFORE vs AFTER

Différence d'usage.

tourne →
BEFORE : modifier :new ou bloquer (garde-fou).
AFTER : propager vers d'autres tables, logger.

Garde-fou

Syntaxe.

tourne →
RAISE_APPLICATION_ERROR(
   -20012,
   'message'
);

Plage codes erreur

Pour RAISE_APPLICATION_ERROR.

tourne →
-20000 à -20999 (réservée par Oracle aux erreurs applicatives).

INSTEAD OF

Quand l'utilise-t-on ?

tourne →
Sur une vue non modifiable (jointure, GROUP BY…). Le trigger remplace le comportement par défaut.

Séquence — syntaxe

Création.

tourne →
CREATE SEQUENCE seq
START WITH 1
INCREMENT BY 1;

NEXTVAL vs CURRVAL

Différence.

tourne →
NEXTVAL = génère + incrémente.
CURRVAL = lit la valeur en cours (sans incrémenter).

Auto-incrément

Pattern trigger + séquence.

tourne →
BEFORE INSERT ON tbl
FOR EACH ROW
BEGIN
   :new.id := seq.NEXTVAL;
END;

Sortie debug

Code pour afficher.

tourne →
DBMS_OUTPUT.PUT_LINE('msg');
-- côté session :
SET SERVEROUTPUT ON;

✎ Quiz éclair

test rapide · 6 questions
0 / 6
1.Pour qu'un trigger puisse accéder à :new et :old, il doit :
  • Être AFTER
  • Être déclaré sur une vue
  • Avoir la clause FOR EACH ROW
  • Utiliser DECLARE
:new et :old ne sont disponibles que pour les triggers de niveau ligne (FOR EACH ROW).
2.Pour gérer un auto-incrément, on combine :
  • Trigger BEFORE INSERT FOR EACH ROW + séquence + NEXTVAL
  • Trigger AFTER INSERT + variable globale
  • Un index unique sur la colonne id
  • Une procédure stockée appelée par l'application
Pattern canonique : BEFORE INSERT FOR EACH ROW qui assigne :new.id := seq.NEXTVAL.
3.Quel code d'erreur est valide pour RAISE_APPLICATION_ERROR ?
  • -1
  • +20012
  • -30000
  • -20012
La plage valide est -20000 à -20999. Seul -20012 est dans cette plage.
4.Un trigger INSTEAD OF s'applique :
  • Sur une table indexée par bitmap
  • À la place d'un BEFORE
  • Sur une vue non directement modifiable
  • Avant les LDD
INSTEAD OF sert à intercepter les écritures sur les vues qui ne sont pas directement modifiables (jointure, GROUP BY, sous-requête).
5.Quelle est la différence entre NEXTVAL et CURRVAL ?
  • Aucune, ce sont des alias
  • NEXTVAL génère et incrémente, CURRVAL ne fait que lire
  • CURRVAL est plus rapide
  • NEXTVAL peut être utilisé sans avoir appelé CURRVAL auparavant — l'inverse n'est pas vrai
L'option 1 est la plus correcte. La 3 est aussi vraie (CURRVAL nécessite un NEXTVAL préalable dans la session) mais c'est secondaire.
6.Quel est le bon ordre dans un bloc PL/SQL ?
  • DECLARE → BEGIN → EXCEPTION → END
  • BEGIN → DECLARE → EXCEPTION → END
  • DECLARE → EXCEPTION → BEGIN → END
  • BEGIN → EXCEPTION → DECLARE → END
Toujours dans cet ordre : déclarations, corps, gestion d'erreurs, fin.

📌 Pour la feuille recto-verso

  • Bloc PL/SQL : DECLARE / BEGIN / EXCEPTION WHEN … THEN / END;
  • %TYPE = type d'une colonne · %ROWTYPE = enregistrement complet
  • RECORD : TYPE r IS RECORD (a t1, b t2)
  • TABLE assoc : TYPE t IS TABLE OF type INDEX BY PLS_INTEGER
  • VARRAY : TYPE v IS VARRAY(n) OF type · indices 1..COUNT
  • 3 boucles : LOOP … EXIT WHEN … END LOOP · FOR i IN a..b LOOP · WHILE cond LOOP
  • Parcours TABLE assoc : i := t.FIRST; WHILE i IS NOT NULL LOOP … i := t.NEXT(i); END LOOP;
  • Affichage : DBMS_OUTPUT.PUT_LINE(...) + SET SERVEROUTPUT ON
  • Trigger : CREATE [OR REPLACE] TRIGGER nom BEFORE|AFTER INSERT|DELETE|UPDATE [OF cols] ON tbl [FOR EACH ROW] [WHEN cond] BEGIN … END;
  • :new et :old uniquement avec FOR EACH ROW
  • Plusieurs événements : BEFORE INSERT OR DELETE + IF INSERTING THEN … ELSIF DELETING THEN …
  • Garde-fou : RAISE_APPLICATION_ERROR(-20012, 'msg'); — plage -20000 à -20999
  • Vue non modifiable → trigger INSTEAD OF
  • Gestion trigger : ALTER TRIGGER … ENABLE/DISABLE/COMPILE · DROP TRIGGER
  • Séquence : CREATE SEQUENCE seq [START WITH n] [INCREMENT BY m]
  • Accès : seq.NEXTVAL (incrémente) · seq.CURRVAL (lit en cours)
  • Auto-incrément pattern : trigger BEFORE INSERT FOR EACH ROW + :new.id := seq.NEXTVAL