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.
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.
Structure générale d'un bloc
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édurePUT_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 %ROWTYPEAu 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
| TYPE | Caractéristiques | Quand l'utiliser |
|---|---|---|
| TABLE | Tableau associatif (index par clé entière) | Taille non connue, accès par clé arbitraire |
| VARRAY | Tableau classique à taille variable mais bornée | Taille 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');
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
Instructions de déclenchement
| Famille | Événements |
|---|---|
| LDD | CREATE, ALTER, DROP |
| LMD | INSERT, DELETE, UPDATE |
| DATABASE | STARTUP, SHUTDOWN, LOGON, … |
Moment de déclenchement autour de l'événement : BEFORE ou AFTER.
Le trigger BEFORE peut modifier les valeurs (:new) ou refuser l'événement. Le trigger AFTER agit après confirmation.
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 :oldDisponibles avec
FOR EACH ROW. :new = nouvelle valeur, :old = ancienne valeur (utile pour UPDATE/DELETE).
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;
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;
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
| Accesseur | Effet |
|---|---|
nom_seq.NEXTVAL | Génère le nombre suivant (incrémente) |
nom_seq.CURRVAL | Ré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;
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 colonnetbl%ROWTYPE = enregistrement complet d'une ligne
TABLE vs VARRAY
Différence clé.
tourne →VARRAY : tableau classique 1..N, taille bornée
3 boucles PL/SQL
Nomme-les.
tourne →LOOP … EXIT WHEN … END LOOPFOR i IN deb..fin LOOPWHILE cond LOOP
Trigger syntaxe
Squelette de base.
tourne →CREATE OR REPLACE TRIGGER nom
BEFORE|AFTER INSERT ON tbl
FOR EACH ROW
BEGIN … END;:new et :old
Quand sont-elles disponibles ?
tourne →FOR EACH ROW.:new = nouvelle valeur, :old = ancienne valeur.
BEFORE vs AFTER
Différence d'usage.
tourne →: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 →INSTEAD OF
Quand l'utilise-t-on ?
tourne →Séquence — syntaxe
Création.
tourne →CREATE SEQUENCE seq
START WITH 1
INCREMENT BY 1;NEXTVAL vs CURRVAL
Différence.
tourne →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
:new et :old, il doit ::new et :old ne sont disponibles que pour les triggers de niveau ligne (FOR EACH ROW).BEFORE INSERT FOR EACH ROW qui assigne :new.id := seq.NEXTVAL.RAISE_APPLICATION_ERROR ?INSTEAD OF s'applique :INSTEAD OF sert à intercepter les écritures sur les vues qui ne sont pas directement modifiables (jointure, GROUP BY, sous-requête).NEXTVAL et CURRVAL ?📌 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; :newet:olduniquement avecFOR 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