Maîtrise du SQL Procédural avec MySQL
Dernière mise à jour : Octobre 2025
Parcours d'apprentissage
Partie 1 : Fondamentaux du SQL Procédural
Partie 2 : La Puissance des Procédures Stockées
Partie 3 : L'Automatisation avec les Déclencheurs
Partie 4 : Fiabilité et Robustesse
Partie 5 : Itération sur les Données
Partie 6 : Administration et Sécurité
Partie 1 : Fondamentaux du SQL Procédural
Chapitre 1 : Accueil & Objectifs Pédagogiques
Bienvenue dans ce cours sur MySQL ! Alors que le SQL standard excelle dans la manipulation de données (lire, insérer, modifier, supprimer), il atteint ses limites lorsqu'il s'agit d'implémenter une logique applicative complexe. Ce cours vous fera passer au niveau supérieur en vous apprenant à programmer directement au sein de votre base de données grâce au SQL procédural.
Objectif Principal
Maîtriser les concepts clés du SQL procédural : fonctions, procédures stockées, triggers, et curseurs. L'objectif est de vous rendre capable d'écrire un code serveur robuste, performant et sécurisé, qui garantit l'intégrité de vos données.
Compétences Visées
À la fin de ce cours, vous saurez automatiser des tâches, gérer des transactions complexes, implémenter des règles métier directement dans la base de données et manipuler les données avec une granularité fine, des compétences essentielles pour tout développeur back-end ou administrateur de bases de données.
Approche Pédagogique
Ce cours privilégie une approche pratique. Chaque concept théorique sera immédiatement illustré par des exemples de code concrets, des exercices d'application et des scénarios réels pour vous aider à assimiler durablement les connaissances.
Chapitre 2 : Blocs d'Instructions et Fonctions
Le SQL procédural nous permet d'exécuter des blocs de code complexes, bien au-delà des simples requêtes. Un bloc est délimité par les mots-clés `BEGIN` et `END`. Pour organiser ce code de manière réutilisable, nous utilisons principalement des **fonctions**.
Une fonction est un sous-programme qui effectue une opération et **retourne toujours une seule valeur**. Elle est idéale pour des calculs, des formatages ou toute logique que l'on souhaite centraliser et réutiliser.
Comme le point-virgule (`;`) termine chaque instruction à l'intérieur du bloc, nous devons temporairement changer le délimiteur standard de MySQL avec la commande `DELIMITER $$`. Cela permet au serveur de comprendre où se termine la définition complète de notre fonction, sans s'arrêter au premier point-virgule rencontré.
2.1. Syntaxe de base d'une fonction
Voici la structure d'une fonction simple. Le mot-clé `DETERMINISTIC` est une indication pour MySQL : il signifie que la fonction retournera toujours le même résultat pour les mêmes paramètres d'entrée, ce qui peut aider le moteur à optimiser son exécution.
-- On change le délimiteur pour pouvoir utiliser ';' à l'intérieur de la fonction
DELIMITER $$
CREATE FUNCTION hello()
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
RETURN 'hello everybody';
END$$
-- On restaure le délimiteur par défaut
DELIMITER ;
-- On appelle la fonction comme n'importe quelle autre fonction native de SQL
SELECT hello();
Fonction avec paramètres
Les fonctions deviennent vraiment puissantes lorsqu'on leur passe des paramètres en entrée. On les déclare simplement entre les parenthèses avec leur nom et leur type.
DROP FUNCTION IF EXISTS hello_one;
DELIMITER $$
CREATE FUNCTION hello_one(name VARCHAR(50))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
RETURN CONCAT('Hello ', name, '!');
END$$
DELIMITER ;
SELECT hello_one('Youssef') AS 'Salutation';
2.2. Déclaration de variables
À l'intérieur d'un bloc `BEGIN...END`, on peut déclarer des variables locales avec `DECLARE`. Il est obligatoire de leur donner un type, et on peut optionnellement leur assigner une valeur par défaut avec `DEFAULT`.
DROP FUNCTION IF EXISTS hello_name_var;
DELIMITER $$
CREATE FUNCTION hello_name_var(name VARCHAR(20))
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
-- Déclaration d'une variable 'salutation' avec une valeur par défaut
DECLARE salutation VARCHAR(50) DEFAULT 'Hello ';
RETURN CONCAT(salutation, name, '!');
END$$
DELIMITER ;
SELECT hello_name_var('Hassan');
2.3. Affectation de valeurs
Après avoir déclaré une variable, on peut lui affecter une valeur de deux manières principales : avec `SET` (la plus commune) ou avec `SELECT ... INTO`.
DROP FUNCTION IF EXISTS division;
DELIMITER $$
CREATE FUNCTION division(a INT, b INT)
RETURNS FLOAT
DETERMINISTIC
BEGIN
DECLARE resultat FLOAT;
-- Affectation avec SET
-- SET resultat = a / b;
-- Affectation alternative avec SELECT ... INTO
SELECT a / b INTO resultat;
RETURN resultat;
END$$
DELIMITER ;
SELECT division(3, 2);
Chapitre 3 : Structures de Contrôle (Conditions & Boucles)
Pour implémenter une logique applicative, il est indispensable de pouvoir contrôler le flux d'exécution du code. MySQL nous offre pour cela des structures conditionnelles et des boucles, similaires à celles que l'on trouve dans les langages de programmation traditionnels.
3.1. Les Conditions : `IF`, `ELSEIF`, `ELSE`
La structure `IF` est la plus fondamentale. Elle permet d'exécuter un bloc de code uniquement si une condition est vraie. On peut l'étendre avec `ELSEIF` pour tester des conditions multiples et `ELSE` pour définir une action par défaut. La structure doit toujours se terminer par `END IF;`.
DROP FUNCTION IF EXISTS division_securisee;
DELIMITER $$
CREATE FUNCTION division_securisee(a INT, b INT)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
-- On vérifie si le diviseur est nul avant de faire le calcul
IF b = 0 THEN
RETURN 'Impossible de diviser par zéro';
ELSE
RETURN a / b;
END IF;
END$$
DELIMITER ;
SELECT division_securisee(3, 0);
3.2. L'alternative `CASE`
Quand on doit vérifier une même variable contre plusieurs valeurs possibles, la structure `CASE` est souvent plus lisible et plus élégante. Elle existe sous deux formes principales :
- `CASE variable WHEN valeur THEN ...` : Idéale pour tester l'égalité avec une série de valeurs.
- `CASE WHEN condition THEN ...` : Plus flexible, permet de tester des conditions complexes (ex: `note > 10 AND note < 12`).
Exemple avec `CASE variable ...`
DROP FUNCTION IF EXISTS getNomJour;
DELIMITER $$
CREATE FUNCTION getNomJour(j INT)
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
DECLARE resultat VARCHAR(100);
SET resultat = CASE j
WHEN 1 THEN 'الأحد'
WHEN 2 THEN 'الإثنين'
WHEN 3 THEN 'الثلاثاء'
WHEN 4 THEN 'الأربعاء'
WHEN 5 THEN 'الخميس'
WHEN 6 THEN 'الجمعة'
WHEN 7 THEN 'السبت'
ELSE 'Numéro de jour invalide'
END;
RETURN resultat;
END$$
DELIMITER ;
Exemple avec `CASE WHEN condition ...`
DROP FUNCTION IF EXISTS getMention;
DELIMITER $$
CREATE FUNCTION getMention(note FLOAT)
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
RETURN CASE
WHEN note < 0 OR note > 20 THEN 'Erreur: note invalide'
WHEN note < 5 THEN 'Très faible'
WHEN note < 9 THEN 'Faible'
WHEN note < 10 THEN 'Insuffisant'
WHEN note < 12 THEN 'Passable'
WHEN note < 14 THEN 'Assez bien'
WHEN note < 16 THEN 'Bien'
WHEN note < 18 THEN 'Très bien'
ELSE 'Excellent'
END;
END$$
DELIMITER ;
SELECT getMention(15.5);
3.3. Les Boucles : `WHILE`, `REPEAT`, et `LOOP`
Les boucles permettent d'exécuter un bloc de code plusieurs fois. MySQL en propose trois types, avec de légères différences :
- WHILE ... END WHILE : La condition est testée avant chaque itération. Si la condition est fausse au départ, la boucle ne s'exécute jamais.
- REPEAT ... UNTIL ... END REPEAT : La boucle s'exécute au moins une fois, car la condition est testée à la fin. La boucle continue *tant que* la condition de fin est fausse.
- LOOP ... END LOOP : Crée une boucle infinie qui doit être quittée manuellement avec `LEAVE nom_de_la_boucle`. C'est utile pour des logiques de sortie plus complexes.
Exemple : Somme des N premiers entiers avec les 3 boucles
-- Avec la boucle WHILE
DROP FUNCTION IF EXISTS somme_while;
DELIMITER $$
CREATE FUNCTION somme_while(n INT) RETURNS BIGINT DETERMINISTIC
BEGIN
DECLARE s BIGINT DEFAULT 0; DECLARE i INT DEFAULT 1;
WHILE i <= n DO SET s = s + i; SET i = i + 1; END WHILE;
RETURN s;
END$$
DELIMITER ;
-- Avec la boucle REPEAT
DROP FUNCTION IF EXISTS somme_repeat;
DELIMITER $$
CREATE FUNCTION somme_repeat(n INT) RETURNS BIGINT DETERMINISTIC
BEGIN
DECLARE s BIGINT DEFAULT 0; DECLARE i INT DEFAULT 1;
REPEAT SET s = s + i; SET i = i + 1; UNTIL i > n END REPEAT;
RETURN s;
END$$
DELIMITER ;
-- Avec la boucle LOOP
DROP FUNCTION IF EXISTS somme_loop;
DELIMITER $$
CREATE FUNCTION somme_loop(n INT) RETURNS BIGINT DETERMINISTIC
BEGIN
DECLARE s BIGINT DEFAULT 0; DECLARE i INT DEFAULT 1;
boucle1: LOOP
IF (i > n) THEN LEAVE boucle1; END IF;
SET s = s + i; SET i = i + 1;
END LOOP boucle1;
RETURN s;
END$$
DELIMITER ;
Ateliers Pratiques : Fonctions, Conditions et Boucles
Il est temps de mettre en pratique tout ce que vous avez appris. Les exercices suivants, tirés de cas concrets, couvrent plusieurs concepts clés et vous aideront à consolider vos compétences de base en SQL procédural.
Exercice 1 : Formatter une date en français
Écrire une fonction qui reçoit une date et la retourne sous la forme "Jour NomDuMois Année" (ex: "12 septembre 2024"). Comparez une approche manuelle avec `CASE` et une approche utilisant les fonctions natives de MySQL.
Approche 1 : Manuelle avec `CASE`
DROP FUNCTION IF EXISTS format_date_manuel;
DELIMITER $$
CREATE FUNCTION format_date_manuel(d DATE)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
DECLARE nom_mois VARCHAR(20);
SET nom_mois = CASE MONTH(d)
WHEN 1 THEN 'janvier' WHEN 7 THEN 'juillet'
WHEN 2 THEN 'février' WHEN 8 THEN 'août'
WHEN 3 THEN 'mars' WHEN 9 THEN 'septembre'
WHEN 4 THEN 'avril' WHEN 10 THEN 'octobre'
WHEN 5 THEN 'mai' WHEN 11 THEN 'novembre'
WHEN 6 THEN 'juin' WHEN 12 THEN 'décembre'
END;
RETURN CONCAT(DAY(d), ' ', nom_mois, ' ', YEAR(d));
END$$
DELIMITER ;
Approche 2 : Avec `DATE_FORMAT` et `lc_time_names` (recommandée)
DROP FUNCTION IF EXISTS date_format_fr;
DELIMITER $$
CREATE FUNCTION date_format_fr (d DATE)
RETURNS VARCHAR(50)
DETERMINISTIC
BEGIN
DECLARE resultat VARCHAR(50);
DECLARE ancienne_locale VARCHAR(10);
SET ancienne_locale = @@lc_time_names;
SET lc_time_names = 'fr_FR';
SET resultat = DATE_FORMAT(d, '%e %M %Y');
SET lc_time_names = ancienne_locale;
RETURN resultat;
END$$
DELIMITER ;
SELECT date_format_fr('2024-02-01');
Exercice 2 : Calculer un écart entre deux dates
Écrire une fonction qui reçoit deux dates (`DATETIME`) et une unité de calcul ('jour', 'mois', 'année', 'heure', etc.) et qui retourne l'écart entre les deux dates selon l'unité demandée.
DROP FUNCTION IF EXISTS calcul_ecart_date;
DELIMITER $$
CREATE FUNCTION calcul_ecart_date(
date_debut DATETIME,
date_fin DATETIME,
unite VARCHAR(20)
)
RETURNS BIGINT
DETERMINISTIC
BEGIN
DECLARE resultat BIGINT;
SET unite = LOWER(unite); -- Rendre la comparaison insensible à la casse
SET resultat = CASE unite
WHEN 'annee' THEN TIMESTAMPDIFF(YEAR, date_debut, date_fin)
WHEN 'mois' THEN TIMESTAMPDIFF(MONTH, date_debut, date_fin)
WHEN 'jour' THEN TIMESTAMPDIFF(DAY, date_debut, date_fin)
WHEN 'heure' THEN TIMESTAMPDIFF(HOUR, date_debut, date_fin)
WHEN 'minute' THEN TIMESTAMPDIFF(MINUTE, date_debut, date_fin)
WHEN 'seconde' THEN TIMESTAMPDIFF(SECOND, date_debut, date_fin)
ELSE NULL -- Retourne NULL si l'unité est invalide
END;
RETURN resultat;
END$$
DELIMITER ;
SELECT calcul_ecart_date('2025-10-10 12:15:00', '2025-10-11 14:18:03', 'heure') AS 'Ecart en Heures';
SELECT calcul_ecart_date('2025-10-10 12:15:00', '2026-11-15 12:15:00', 'Mois') AS 'Ecart en Mois';
Exercices sur la Base de Données `vols`
Les exercices suivants nécessitent la base de données `vols`. Exécutez le script ci-dessous pour la mettre en place.
DROP DATABASE IF EXISTS vols;
CREATE DATABASE vols COLLATE utf8mb4_general_ci;
USE vols;
CREATE TABLE Pilote(numpilote INT AUTO_INCREMENT PRIMARY KEY, nom VARCHAR(50), villepilote VARCHAR(50), datedebut DATE);
CREATE TABLE Avion(numav INT AUTO_INCREMENT PRIMARY KEY, typeav VARCHAR(50), capav INT);
CREATE TABLE Vol(numvol INT AUTO_INCREMENT PRIMARY KEY, numpil INT NOT NULL, numav INT NOT NULL);
ALTER TABLE Vol ADD CONSTRAINT fk_vol_pilote FOREIGN KEY(numpil) REFERENCES Pilote(numpilote);
ALTER TABLE Vol ADD CONSTRAINT fk_vol_avion FOREIGN KEY(numav) REFERENCES Avion(numav);
INSERT INTO Avion(numav, typeav, capav) VALUES (1,'Boeing',350), (2,'Caravelle',250), (3,'Airbus',500), (4,'Concorde',150);
INSERT INTO Pilote(numpilote, nom, villepilote, datedebut) VALUES (1,'Hassan','Tétouan','2022-01-01'), (2,'Saida','Casablanca','2005-01-01'), (3,'Youssef','Tanger','2002-01-01');
INSERT INTO Vol(numvol, numpil, numav) VALUES (1,1,1), (2,1,1), (3,2,2), (4,2,2), (5,3,3), (6,3,3), (7,1,1), (8,1,1), (9,1,2), (10,1,2), (11,3,3), (12,3,3), (13,2,1), (14,3,1);
Exercice 3 : Nombre de pilotes par volume de vols
Écrire une fonction qui retourne le nombre de pilotes ayant effectué un nombre de vols strictement supérieur à un seuil donné en paramètre.
DROP FUNCTION IF EXISTS q1_nbpilotes;
DELIMITER $$
CREATE FUNCTION q1_nbpilotes (nb INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE total INT;
-- On utilise une sous-requête (ou une CTE) pour d'abord compter les vols par pilote
SELECT COUNT(*) INTO total FROM (
SELECT numpil
FROM Vol
GROUP BY numpil
HAVING COUNT(*) > nb
) AS PilotesFiltres;
RETURN total;
END$$
DELIMITER ;
SELECT q1_nbpilotes(5); -- Pilote 1 a 6 vols > 5. Résultat: 1
SELECT q1_nbpilotes(3); -- Pilote 1 (6), Pilote 3 (5). Résultat: 2
Exercice 4 : Ancienneté d'un pilote
Écrire une fonction qui retourne l'ancienneté (en années complètes) d'un pilote dont l'ID est passé en paramètre.
DROP FUNCTION IF EXISTS q2_duree_travail_annees;
DELIMITER $$
CREATE FUNCTION q2_duree_travail_annees(id_pilote INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE duree INT;
SELECT TIMESTAMPDIFF(YEAR, datedebut, CURDATE()) INTO duree
FROM Pilote
WHERE numpilote = id_pilote;
RETURN duree;
END$$
DELIMITER ;
SELECT q2_duree_travail_annees(3); -- Pilote Youssef, a commencé en 2002
Exercice 5 : Avions non affectés
Écrire une fonction qui renvoie le nombre des avions qui ne sont pas du tout affectés à des vols.
DROP FUNCTION IF EXISTS q3_avion_non_affectes;
DELIMITER $$
CREATE FUNCTION q3_avion_non_affectes()
RETURNS INT
READS SQL DATA
BEGIN
DECLARE total INT;
SELECT COUNT(*) INTO total
FROM Avion
WHERE numav NOT IN (SELECT DISTINCT numav FROM Vol);
RETURN total;
END$$
DELIMITER ;
SELECT q3_avion_non_affectes(); -- Avion 4 n'a pas de vol. Résultat: 1
Exercice 6 : Pilote le plus ancien sur un avion
Écrire une fonction qui retourne le numéro du pilote le plus ancien (basé sur `datedebut`) ayant piloté un avion dont le numéro est passé en paramètre.
DROP FUNCTION IF EXISTS q4_plus_ancien_pilote_sur_avion;
DELIMITER $$
CREATE FUNCTION q4_plus_ancien_pilote_sur_avion(id_avion INT)
RETURNS INT
READS SQL DATA
BEGIN
DECLARE id_pilote INT;
SELECT p.numpilote INTO id_pilote
FROM Pilote p
JOIN Vol v ON p.numpilote = v.numpil
WHERE v.numav = id_avion
ORDER BY p.datedebut ASC
LIMIT 1;
RETURN id_pilote;
END$$
DELIMITER ;
-- Sur l'avion 1, pilotes 1, 2, 3 ont volé. Le plus ancien est Youssef (3) qui a commencé en 2002.
SELECT q4_plus_ancien_pilote_sur_avion(1);
Exercices sur la Base de Données `employes`
Les exercices suivants nécessitent la base de données `employes`. Exécutez le script ci-dessous pour la mettre en place.
DROP DATABASE IF EXISTS employes;
CREATE DATABASE employes COLLATE 'utf8mb4_general_ci';
USE employes;
CREATE TABLE DEPARTEMENT (ID_DEP INT AUTO_INCREMENT PRIMARY KEY, NOM_DEP VARCHAR(50), Ville VARCHAR(50));
CREATE TABLE EMPLOYE (ID_EMP INT AUTO_INCREMENT PRIMARY KEY, NOM_EMP VARCHAR(50), PRENOM_EMP VARCHAR(50), SALAIRE FLOAT, ID_DEP INT, CONSTRAINT fk_emp_dep FOREIGN KEY (ID_DEP) REFERENCES DEPARTEMENT(ID_DEP));
INSERT INTO DEPARTEMENT(NOM_DEP, Ville) VALUES ('FINANCIER','Tanger'), ('Informatique','Tétouan'), ('Marketing','Martil'), ('GRH','Mdiq');
INSERT INTO EMPLOYE(NOM_EMP, PRENOM_EMP, SALAIRE, ID_DEP) VALUES
('said','said',8000,1), ('hassan','hassan',8500,1), ('khalid','khalid',7000,2),
('souad','souad',6500,2), ('Farida','Farida',5000,3), ('Amal','Amal',6000,4), ('Mohamed','Mohamed',7000,4);
Exercice 7 : Création de fonctions statistiques globales
Créez quatre fonctions distinctes pour l'ensemble des employés : `nombre_total_employes()`, `somme_salaires()`, `salaire_minimum()` et `salaire_maximum()`. Puis, utilisez-les dans une seule requête pour afficher un tableau de bord global.
Les 4 fonctions
-- 1. Nombre total d'employés
DROP FUNCTION IF EXISTS nombre_total_employes;
DELIMITER $$
CREATE FUNCTION nombre_total_employes() RETURNS INT READS SQL DATA
BEGIN RETURN (SELECT COUNT(*) FROM EMPLOYE); END$$
DELIMITER ;
-- 2. Somme totale des salaires
DROP FUNCTION IF EXISTS somme_salaires;
DELIMITER $$
CREATE FUNCTION somme_salaires() RETURNS FLOAT READS SQL DATA
BEGIN RETURN (SELECT SUM(SALAIRE) FROM EMPLOYE); END$$
DELIMITER ;
-- 3. Salaire minimum
DROP FUNCTION IF EXISTS salaire_minimum;
DELIMITER $$
CREATE FUNCTION salaire_minimum() RETURNS FLOAT READS SQL DATA
BEGIN RETURN (SELECT MIN(SALAIRE) FROM EMPLOYE); END$$
DELIMITER ;
-- 4. Salaire maximum
DROP FUNCTION IF EXISTS salaire_maximum;
DELIMITER $$
CREATE FUNCTION salaire_maximum() RETURNS FLOAT READS SQL DATA
BEGIN RETURN (SELECT MAX(SALAIRE) FROM EMPLOYE); END$$
DELIMITER ;
Requête d'utilisation
SELECT
nombre_total_employes() AS 'Nb Employés',
somme_salaires() AS 'Masse Salariale',
salaire_minimum() AS 'Salaire Min',
salaire_maximum() AS 'Salaire Max';
Exercice 8 : Création de fonctions statistiques par département
De la même manière, créez quatre fonctions paramétrées par le nom du département (`NOM_DEP`) pour obtenir les mêmes statistiques, mais cette fois-ci, spécifiques à un département. Puis, utilisez-les pour générer un rapport complet par département.
Les 4 fonctions paramétrées
-- 1. Nombre d'employés par département
DROP FUNCTION IF EXISTS q6_nb_s;
DELIMITER $$
CREATE FUNCTION q6_nb_s(nom_d VARCHAR(50)) RETURNS INT READS SQL DATA
BEGIN RETURN (SELECT COUNT(*) FROM EMPLOYE JOIN DEPARTEMENT USING(ID_DEP) WHERE NOM_DEP = nom_d); END$$
DELIMITER ;
-- 2. Somme des salaires par département
DROP FUNCTION IF EXISTS q7_sum_s;
DELIMITER $$
CREATE FUNCTION q7_sum_s(nom_d VARCHAR(50)) RETURNS FLOAT READS SQL DATA
BEGIN RETURN (SELECT SUM(SALAIRE) FROM EMPLOYE JOIN DEPARTEMENT USING(ID_DEP) WHERE NOM_DEP = nom_d); END$$
DELIMITER ;
-- Et ainsi de suite pour MIN et MAX...
Rapport final par département
-- Note: Les fonctions pour MIN(q8_min_s) et MAX(q9_max_s) doivent aussi être créées sur le même modèle.
SELECT
UPPER(NOM_DEP) AS 'Département',
q6_nb_s(NOM_DEP) AS 'Nb Employés',
q7_sum_s(NOM_DEP) AS 'Masse Salariale',
q8_min_s(NOM_DEP) AS 'Salaire Min',
q9_max_s(NOM_DEP) AS 'Salaire Max'
FROM
DEPARTEMENT;
Partie 2 : La Puissance du SQL Procédural : Les Procédures Stockées
Chapitre 4 : Introduction aux Procédures Stockées
Alors que les fonctions sont excellentes pour retourner une valeur unique, les procédures stockées offrent une flexibilité et une puissance bien plus grandes. Une procédure stockée est un ensemble d'instructions SQL précompilées, stockées directement dans la base de données. Elles peuvent exécuter des actions complexes, manipuler des données (`INSERT`, `UPDATE`, `DELETE`), et même retourner plusieurs jeux de résultats.
Performance Accrue
Le code est analysé et optimisé une seule fois lors de sa création. Les exécutions suivantes sont beaucoup plus rapides, ce qui réduit la charge sur le serveur et le trafic réseau.
Réutilisabilité et Maintenance
Une logique métier complexe peut être centralisée dans une procédure. Si les règles changent, il suffit de mettre à jour la procédure, sans avoir à modifier le code de toutes les applications qui l'utilisent.
Sécurité Renforcée
On peut accorder aux utilisateurs le droit d'exécuter une procédure stockée sans leur donner un accès direct aux tables sous-jacentes, prévenant ainsi les manipulations de données non désirées.
4.1. Syntaxe de base et exécution
La syntaxe est similaire à celle d'une fonction, mais sans la clause `RETURNS`. On utilise le mot-clé `CREATE PROCEDURE`. Pour l'exécuter, on utilise la commande `CALL` suivie du nom de la procédure.
-- On change le délimiteur
DELIMITER $$
CREATE PROCEDURE liste_complete()
BEGIN
-- Une procédure peut retourner plusieurs jeux de résultats
SELECT * FROM cheval;
SELECT * FROM jockey;
END$$
-- On restaure le délimiteur
DELIMITER ;
-- On appelle la procédure pour l'exécuter
CALL liste_complete();
Chapitre 5 : Paramètres des Procédures Stockées (`IN`, `OUT`)
La véritable puissance des procédures vient de leur capacité à accepter des paramètres. MySQL définit trois modes de passage de paramètres :
- `IN` : Le paramètre est en lecture seule. C'est une valeur que l'on passe à la procédure. C'est le mode par défaut.
- `OUT` : Le paramètre est en écriture seule. La procédure peut lui assigner une valeur qui sera accessible à l'extérieur après l'appel.
- `INOUT` : Un mélange des deux. On passe une valeur qui peut être lue et modifiée par la procédure.
5.1. Paramètres d'entrée `IN`
C'est le cas le plus courant. On utilise la valeur passée pour filtrer une requête, effectuer un calcul ou insérer une donnée.
DROP PROCEDURE IF EXISTS proprio;
DELIMITER $$
CREATE PROCEDURE proprio(IN id_cheval_recherche INT)
BEGIN
SELECT NOM_PROPRIETAIRE, PRENOM_PRORIETAIRE
FROM proprietaire p
JOIN cheval c USING(id_proprietaire)
WHERE id_cheval = id_cheval_recherche;
END$$
DELIMITER ;
CALL proprio(3);
5.2. Paramètres de sortie `OUT`
Les paramètres `OUT` sont le principal moyen pour une procédure de "retourner" des valeurs scalaires. Lors de l'appel, on doit fournir une variable de session (préfixée par `@`) pour stocker le résultat.
DROP PROCEDURE IF EXISTS somme;
DELIMITER $$
CREATE PROCEDURE somme(
IN a INT,
IN b INT,
OUT resultat INT
)
BEGIN
SET resultat = a + b;
END$$
DELIMITER ;
-- On appelle la procédure en passant une variable @s pour recevoir le résultat
CALL somme(3, 5, @s);
-- On affiche le contenu de la variable de session
SELECT @s AS 'Résultat de la somme';
Exemple avec plusieurs paramètres `OUT`
DROP PROCEDURE IF EXISTS calcules;
DELIMITER $$
CREATE PROCEDURE calcules(
IN x INT, IN y INT,
OUT s INT, OUT m INT, OUT a INT, OUT d FLOAT
)
BEGIN
SET s = x - y;
SET m = x * y;
SET a = x + y;
SET d = x / y;
END$$
DELIMITER ;
CALL calcules(10, 5, @soustraction, @multiplication, @addition, @division);
SELECT @soustraction, @multiplication, @addition, @division;
Chapitre 6 : Logique Applicative et Manipulation de Données
C'est ici que les procédures stockées révèlent tout leur potentiel. Elles permettent d'encapsuler une logique métier complexe, incluant des conditions, des boucles, et des opérations de modification de données, tout en garantissant l'atomicité et l'intégrité des opérations.
6.1. `SELECT ... INTO` pour récupérer une valeur
Une des constructions les plus utiles dans une procédure est `SELECT ... INTO`. Elle permet d'exécuter une requête qui retourne une seule ligne et d'affecter les valeurs de ses colonnes à des variables locales. C'est le moyen le plus direct de récupérer une donnée de la base pour l'utiliser dans la logique de la procédure.
-- Cette procédure récupère le stock actuel dans une variable AVANT de le modifier
DROP PROCEDURE IF EXISTS ex2_vente;
DELIMITER $$
CREATE PROCEDURE ex2_vente(IN num_produit INT, IN qte_vendue INT)
BEGIN
DECLARE stock_actuel INT;
-- On récupère la valeur du stock de la table et on la met dans notre variable
SELECT stock INTO stock_actuel FROM produit WHERE numproduit = num_produit;
IF stock_actuel < qte_vendue THEN
SELECT 'Opération impossible : stock insuffisant' AS 'message';
ELSE
UPDATE produit SET stock = stock_actuel - qte_vendue WHERE numproduit = num_produit;
IF stock_actuel - qte_vendue < 10 THEN
SELECT CONCAT('Besoin de réapprovisionnement, stock restant : ', stock_actuel - qte_vendue) AS 'message';
ELSE
SELECT CONCAT('Opération effectuée avec succès, stock restant : ', stock_actuel - qte_vendue) AS 'message';
END IF;
END IF;
END$$
DELIMITER ;
CALL ex2_vente(4, 15); -- Vente de 15 PC (stock 20) -> succès, stock restant 5 -> réapprovisionnement
CALL ex2_vente(4, 10); -- Vente de 10 PC (stock 5) -> impossible
6.2. Suppression de données
Les procédures sont idéales pour sécuriser les opérations de suppression. On peut y ajouter des vérifications, de la journalisation, ou des suppressions en cascade complexes.
DROP PROCEDURE IF EXISTS q4_ps_supprimer_produit;
DELIMITER &&
CREATE PROCEDURE q4_ps_supprimer_produit(num_prod INT)
BEGIN
DELETE FROM produit WHERE numProduit = num_prod;
END&&
DELIMITER ;
CALL q4_ps_supprimer_produit(2); -- Supprime le produit 'chaise'
SELECT * FROM produit;
Ateliers Pratiques : Procédures Stockées
Passons à la pratique. Les exercices suivants vous permettront de créer des procédures stockées pour des cas d'usage variés : affichage, modification de données, calculs et logique métier.
Exercices 1 à 5 : Base de Données `produits`
Les exercices suivants utilisent la base de données `produits`. Exécutez le script ci-dessous pour la mettre en place.
DROP DATABASE IF EXISTS produits;
CREATE DATABASE produits COLLATE utf8mb4_general_ci;
USE produits;
CREATE TABLE Produit(
numProduit INT AUTO_INCREMENT PRIMARY KEY,
libelle VARCHAR(50),
PU FLOAT,
stock INT
);
INSERT INTO produit VALUES
(1,'table',350,100), (2,'chaise',100,10), (3,'armoire',2350,10),
(4,'pc',3500,20), (5,'clavier',150,200), (6,'souris',50,200),
(7,'ecran',2350,70), (8,'scanner',1350,5), (9,'imprimante',950,5);
Exercice 1 : Procédures d'affichage simple
1. Écrire une PS qui affiche tous les produits.
2. Écrire une PS qui affiche les libellés des produits dont le stock est inférieur à 10.
3. Écrire une PS qui admet en paramètre un numéro de produit et affiche un message contenant le libellé, le prix et le stock.
4. Écrire une PS qui permet de supprimer un produit en passant son numéro en paramètre.
-- 1. Afficher tous les produits
DROP PROCEDURE IF EXISTS q1_prdt;
DELIMITER &&
CREATE PROCEDURE q1_prdt()
BEGIN
SELECT * FROM produit;
END&&
DELIMITER ;
CALL q1_prdt();
-- 2. Afficher les produits avec stock faible
DROP PROCEDURE IF EXISTS q2_stockfaible;
DELIMITER &&
CREATE PROCEDURE q2_stockfaible()
BEGIN
SELECT libelle FROM produit WHERE stock < 10;
END&&
DELIMITER ;
CALL q2_stockfaible();
-- 3. Afficher les détails d'un produit spécifique
DROP PROCEDURE IF EXISTS q3_aff_prod;
DELIMITER &&
CREATE PROCEDURE q3_aff_prod(num_prod INT)
BEGIN
SELECT CONCAT('Libellé: ', libelle, ", PU: ", pu, ", Stock: ", stock) AS 'message'
FROM produit
WHERE numProduit = num_prod;
END&&
DELIMITER ;
CALL q3_aff_prod(3);
-- 4. Supprimer un produit (déjà vu au chapitre 6)
-- CALL q4_ps_supprimer_produit(9);
Exercice 2 : Mettre à jour le stock après une vente
Écrire une PS qui gère une vente. Elle reçoit en paramètre le numéro du produit et la quantité à vendre, puis retourne un message de statut : 'Opération impossible', 'Besoin de réapprovisionnement', ou 'Opération effectuée avec succès'.
-- La solution est la procédure ex2_vente détaillée au chapitre 6.
DROP PROCEDURE IF EXISTS ex2_vente;
DELIMITER $$
CREATE PROCEDURE ex2_vente(IN num INT, IN qte INT)
BEGIN
DECLARE stock_reste INT;
SELECT stock INTO stock_reste FROM produit WHERE numproduit=num;
IF stock_reste < qte THEN
SELECT 'Opération impossible' AS 'message';
ELSE
UPDATE produit SET stock= stock_reste - qte WHERE numproduit=num;
IF stock_reste-qte <10 THEN
SELECT CONCAT('Besoin de réapprovisionnement, reste : ', stock_reste-qte) AS 'message';
ELSE
SELECT CONCAT('Opération effectuée avec succès, reste : ', stock_reste-qte) AS 'message';
END IF;
END IF;
END$$
DELIMITER ;
CALL ex2_vente(4, 11);
Exercice 3 : Calculer le prix moyen
Écrire une PS qui retourne le prix moyen des produits en utilisant un paramètre `OUT`.
DROP PROCEDURE IF EXISTS ex3_moyenne;
DELIMITER $$
CREATE PROCEDURE ex3_moyenne(OUT moyenne FLOAT)
BEGIN
SELECT AVG(pu) INTO moyenne FROM produit;
END$$
DELIMITER ;
CALL ex3_moyenne(@m);
SELECT @m AS 'Prix Unitaire Moyen';
Exercice 4 : Calculer le factoriel d'un nombre
Créer une procédure stockée qui accepte un entier et retourne son factoriel. Gérer le cas où le nombre est négatif.
DROP PROCEDURE IF EXISTS ex4_factoriel;
DELIMITER //
CREATE PROCEDURE ex4_factoriel(IN nb INT, OUT factoriel BIGINT)
BEGIN
DECLARE i INT DEFAULT 1;
IF nb < 0 THEN
SET factoriel = NULL; -- Factoriel n'est pas défini pour les négatifs
ELSE
SET factoriel = 1;
REPEAT
SET factoriel = i * factoriel;
SET i = i + 1;
UNTIL i > nb END REPEAT;
END IF;
END//
DELIMITER ;
CALL ex4_factoriel(5, @f);
SELECT @f; -- Résultat: 120
Exercice 5 : Une calculatrice universelle
Créer une procédure qui reçoit 2 entiers, un opérateur sous forme de chaîne ('+', '-', '*', '/', '%') et retourne le résultat du calcul dans un paramètre `OUT`. Gérer les divisions par zéro.
DROP PROCEDURE IF EXISTS calcule;
DELIMITER $$
CREATE PROCEDURE calcule(IN a INT, IN b INT, IN op CHAR(1), OUT resultat FLOAT)
BEGIN
IF op IN ('/', '%') AND b=0 THEN
SET resultat = NULL;
ELSE
SET resultat = CASE op
WHEN '+' THEN a+b
WHEN '/' THEN a/b
WHEN '*' THEN a*b
WHEN '-' THEN a-b
WHEN '%' THEN a%b
ELSE NULL
END;
END IF;
END$$
DELIMITER ;
CALL calcule(5, 0, '/', @res);
SELECT @res; -- Résultat: NULL
CALL calcule(10, 3, '%', @res);
SELECT @res; -- Résultat: 1
Exercice 6 : Base de Données `cuisine`
L'exercice suivant est un cas d'étude complet sur la gestion de recettes de cuisine. Exécutez le script ci-dessous pour mettre en place la base de données `cuisine`.
DROP DATABASE IF EXISTS cuisine;
CREATE DATABASE cuisine;
USE cuisine;
CREATE TABLE Fournisseur (NumFou INT AUTO_INCREMENT PRIMARY KEY, RSFou VARCHAR(50), AdrFou VARCHAR(100));
CREATE TABLE Recettes (NumRec INT AUTO_INCREMENT PRIMARY KEY, NomRec VARCHAR(50), MethodePreparation VARCHAR(60), TempsPreparation INT);
CREATE TABLE Ingredients (NumIng INT AUTO_INCREMENT PRIMARY KEY, NomIng VARCHAR(50), PUIng FLOAT, UniteMesureIng VARCHAR(20), NumFou INT, CONSTRAINT fk_ing_fou FOREIGN KEY (NumFou) REFERENCES Fournisseur(NumFou));
CREATE TABLE Composition_Recette (NumRec INT NOT NULL, NumIng INT NOT NULL, QteUtilisee FLOAT, CONSTRAINT fk_cr_rec FOREIGN KEY (NumRec) REFERENCES Recettes(NumRec), CONSTRAINT fk_cr_ing FOREIGN KEY (NumIng) REFERENCES Ingredients(NumIng), CONSTRAINT pk_cr PRIMARY KEY (NumIng,NumRec));
-- Les instructions INSERT sont dans le fichier original
Exercice 6 : Cas d'étude - Gestion de recettes
Créer une série de procédures stockées pour gérer et analyser les recettes de cuisine.
PS1 : Affiche la liste des ingrédients avec le nom de leur fournisseur.
PS2 : Affiche pour chaque recette son nom, le nombre d'ingrédients et son coût total.
PS4 : Reçoit un numéro de recette et retourne son nom.
PS6 : Reçoit un numéro de recette et affiche la liste de ses ingrédients (nom, quantité, montant).
PS7 : Reçoit un numéro de recette et affiche une fiche complète en appelant les autres procédures.
PS8 : Gère les fournisseurs : vérifie son existence et, s'il n'a pas d'ingrédients, le supprime.
PS9 : Affiche une fiche de synthèse pour une recette donnée.
PS1 : Affiche la liste des ingrédients avec le nom de leur fournisseur.
DROP PROCEDURE IF EXISTS ps1;
DELIMITER $$
CREATE PROCEDURE ps1()
BEGIN
SELECT i.NumIng, i.NomIng, f.RSFou FROM ingredients i JOIN fournisseur f USING (numfou);
END$$
DELIMITER ;
CALL ps1();
PS2 : Affiche pour chaque recette son nom, le nombre d'ingrédients et son coût total.
DROP PROCEDURE IF EXISTS ps2;
DELIMITER $$
CREATE PROCEDURE ps2()
BEGIN
SELECT numrec, nomrec, COUNT(cr.numing) AS 'Nombre Ingredients', SUM(qteUtilisee * PUIng) AS 'Coût Total'
FROM recettes r
LEFT JOIN composition_recette cr USING (numrec)
LEFT JOIN ingredients i USING (numing)
GROUP BY numrec, nomrec;
END$$
DELIMITER ;
CALL ps2();
PS4 : Reçoit un numéro de recette et retourne son nom.
DROP PROCEDURE IF EXISTS ps4;
DELIMITER $$
CREATE PROCEDURE ps4(IN n INT, OUT nom VARCHAR(50))
BEGIN
SELECT NomRec INTO nom FROM recettes WHERE NumRec=n;
END$$
DELIMITER ;
CALL ps4(2, @r);
SELECT @r;
PS6 : Reçoit un numéro de recette et affiche la liste de ses ingrédients (nom, quantité, montant).
DROP PROCEDURE IF EXISTS ps6;
DELIMITER $$
CREATE PROCEDURE ps6(IN num_recette INT)
BEGIN
SELECT
i.NomIng,
cr.QteUtilisee,
(i.PUIng * cr.QteUtilisee) AS 'montant'
FROM composition_recette cr
JOIN ingredients i USING (numing)
WHERE cr.numrec = num_recette;
END$$
DELIMITER ;
CALL ps6(1);
PS7 : Reçoit un numéro de recette et affiche une fiche complète en appelant les autres procédures.
DROP PROCEDURE IF EXISTS ps7;
DELIMITER $$
CREATE PROCEDURE ps7(IN recette INT)
BEGIN
CALL ps4(recette, @nom);
SELECT @nom AS 'Nom de la Recette';
CALL ps6(recette);
END$$
DELIMITER ;
CALL ps7(3);
PS8 : Gère les fournisseurs : vérifie son existence et, s'il n'a pas d'ingrédients, le supprime.
DROP PROCEDURE IF EXISTS ps8;
DELIMITER $$
CREATE PROCEDURE ps8(IN f INT)
BEGIN
IF NOT EXISTS (SELECT * FROM fournisseur WHERE numfou = f) THEN
SELECT 'Aucun fournisseur ne porte ce numéro' AS 'message';
ELSE
IF NOT EXISTS (SELECT * FROM ingredients WHERE numfou = f) THEN
SELECT 'Ce fournisseur n\'a aucun ingrédient. Il sera supprimé' AS 'message';
DELETE FROM fournisseur WHERE numfou = f;
ELSE
SELECT NumIng, NomIng FROM ingredients WHERE numfou = f;
END IF;
END IF;
END$$
DELIMITER ;
INSERT INTO fournisseur VALUES (4, 'Testeur', 'test');
CALL ps8(4); -- Va supprimer le fournisseur 4
CALL ps8(1); -- Va lister les ingrédients du fournisseur 1
PS9 : Affiche une fiche de synthèse pour une recette donnée.
DROP PROCEDURE IF EXISTS ps9;
DELIMITER $$
CREATE PROCEDURE ps9(IN num_recette INT)
BEGIN
DECLARE v_nom_rec VARCHAR(50);
DECLARE v_temps INT;
DECLARE v_methode VARCHAR(60);
DECLARE v_cout_total FLOAT;
-- Récupérer les informations de la recette
SELECT NomRec, TempsPreparation, MethodePreparation
INTO v_nom_rec, v_temps, v_methode
FROM Recettes WHERE NumRec = num_recette;
-- Calculer le coût total de la recette
SELECT SUM(cr.QteUtilisee * i.PUIng)
INTO v_cout_total
FROM Composition_Recette cr
JOIN Ingredients i ON cr.NumIng = i.NumIng
WHERE cr.NumRec = num_recette;
-- Afficher les messages et les résultats
SELECT CONCAT('Recette : ', v_nom_rec, ', temps de préparation : ', v_temps, ' minutes') AS 'Information';
-- Afficher la liste des ingrédients
SELECT i.NomIng, cr.QteUtilisee
FROM Composition_Recette cr
JOIN Ingredients i ON cr.NumIng = i.NumIng
WHERE cr.NumRec = num_recette;
SELECT CONCAT('Sa méthode de préparation est : ', v_methode) AS 'Préparation';
IF v_cout_total < 50 THEN
SELECT 'Prix intéressant' AS 'Avis sur le coût';
ELSE
SELECT 'Prix élevé' AS 'Avis sur le coût';
END IF;
END$$
DELIMITER ;
CALL ps9(1);
Partie 3 : L'Automatisation avec les Déclencheurs (Triggers)
Chapitre 7 : Introduction aux Déclencheurs
Imaginez pouvoir exécuter un bloc de code automatiquement chaque fois qu'un événement spécifique se produit sur une table, comme l'ajout, la modification ou la suppression d'une ligne. C'est exactement ce que permettent les **déclencheurs**, ou **triggers**. Ils sont la clé pour garantir l'intégrité des données et automatiser des règles métier complexes directement au niveau de la base de données.
Intégrité des Données
Les triggers peuvent imposer des règles de validation plus complexes que les contraintes standards, assurant que les données restent toujours cohérentes.
Automatisation
Ils permettent d'automatiser des actions en cascade. Par exemple, mettre à jour un stock après une vente, calculer un champ dérivé ou archiver une ligne supprimée.
Journalisation (Audit)
Un trigger est l'outil parfait pour tracer les modifications sur des données sensibles en enregistrant qui a modifié quoi et quand dans une table d'audit.
7.1. Syntaxe et Événements
Un trigger est associé à une table et se déclenche à un moment précis pour un événement donné :
- Moment du déclenchement : `BEFORE` (avant l'opération) ou `AFTER` (après l'opération).
- Événement : `INSERT`, `UPDATE`, ou `DELETE`.
CREATE TRIGGER nom_du_trigger
{BEFORE | AFTER} {INSERT | UPDATE | DELETE}
ON nom_de_la_table
FOR EACH ROW
BEGIN
-- Instructions à exécuter...
END;
7.2. Les alias `NEW` et `OLD`
À l'intérieur d'un trigger, MySQL nous donne accès à des alias spéciaux pour manipuler les données concernées par l'événement. Leur disponibilité dépend de l'événement :
- Pour un `INSERT` : Seul `NEW` est disponible. `NEW.colonne` représente la valeur de la colonne de la nouvelle ligne qui est en train d'être insérée.
- Pour un `DELETE` : Seul `OLD` est disponible. `OLD.colonne` représente la valeur de la colonne de la ligne qui est en train d'être supprimée.
- Pour un `UPDATE` : `OLD` et `NEW` sont tous deux disponibles. `OLD.colonne` est la valeur avant la modification, et `NEW.colonne` est la nouvelle valeur proposée.
Chapitre 8 : Triggers en Action : Gestion de Stock
Le cas d'usage le plus classique pour les triggers est la gestion d'un stock. Nous allons créer une base de données simple `ventes` et automatiser la mise à jour du stock de nos produits à chaque opération de vente.
Mise en place de la base `ventes`
DROP DATABASE IF EXISTS ventes;
CREATE DATABASE ventes COLLATE utf8mb4_general_ci;
USE ventes;
CREATE TABLE produit(
id_produit INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50),
prix FLOAT,
stock INT,
CHECK (stock >= 0) -- Contrainte pour ne jamais avoir de stock négatif
);
CREATE TABLE vente(
id_vente INT AUTO_INCREMENT PRIMARY KEY,
date_vente DATETIME DEFAULT CURRENT_TIMESTAMP,
qte INT,
id_produit INT,
CONSTRAINT fk_vente_produit FOREIGN KEY (id_produit) REFERENCES produit(id_produit)
);
INSERT INTO produit VALUES (1,'chaise',200,20), (2,'table',1500,10), (3,'armoire',5000,5);
8.1. Le Déclencheur `AFTER INSERT`
Chaque fois qu'une nouvelle vente est insérée dans la table `vente`, nous devons décrémenter le stock du produit correspondant dans la table `produit`.
DROP TRIGGER IF EXISTS after_insert_vente;
DELIMITER $$
CREATE TRIGGER after_insert_vente
AFTER INSERT ON vente
FOR EACH ROW
BEGIN
UPDATE produit SET stock = stock - NEW.qte WHERE id_produit = NEW.id_produit;
END$$
DELIMITER ;
-- Test : vendons 2 tables (stock initial : 10)
INSERT INTO vente (qte, id_produit) VALUES (2, 2);
SELECT * FROM produit WHERE id_produit = 2; -- Le stock est maintenant à 8
8.2. Le Déclencheur `AFTER DELETE`
Si une vente est annulée (supprimée de la table `vente`), il faut logiquement remettre les produits dans le stock.
DROP TRIGGER IF EXISTS after_delete_vente;
DELIMITER $$
CREATE TRIGGER after_delete_vente
AFTER DELETE ON vente
FOR EACH ROW
BEGIN
UPDATE produit SET stock = stock + OLD.qte WHERE id_produit = OLD.id_produit;
END$$
DELIMITER ;
-- Test : annulons la vente précédente (id_vente = 1, qte = 2)
DELETE FROM vente WHERE id_vente = 1;
SELECT * FROM produit WHERE id_produit = 2; -- Le stock est de retour à 10
8.3. Le Déclencheur `AFTER UPDATE`
Si la quantité d'une vente est modifiée, le stock doit être ajusté en conséquence. Nous devons soustraire la nouvelle quantité et ajouter l'ancienne.
DROP TRIGGER IF EXISTS after_update_vente;
DELIMITER $$
CREATE TRIGGER after_update_vente
AFTER UPDATE ON vente
FOR EACH ROW
BEGIN
-- Formule : stock_final = stock_actuel - (nouvelle_qte - ancienne_qte)
UPDATE produit SET stock = stock - (NEW.qte - OLD.qte)
WHERE id_produit = NEW.id_produit;
END$$
DELIMITER ;
-- Test : vendons 5 chaises (stock: 20 -> 15)
INSERT INTO vente (qte, id_produit) VALUES (5, 1);
-- Modifions la vente : le client en veut 8 au lieu de 5 (+3)
UPDATE vente SET qte = 8 WHERE id_vente = 2;
SELECT * FROM produit WHERE id_produit = 1; -- Le stock est maintenant à 12 (15 - (8-5))
8.4. Triggers et Transactions : une alliance puissante
Un point crucial : **l'instruction qui déclenche le trigger et le trigger lui-même s'exécutent au sein d'une seule et même transaction.** Si une erreur survient dans le trigger (par exemple, la mise à jour du stock viole la contrainte `CHECK (stock >= 0)`), l'opération initiale (l'`INSERT` de la vente) est entièrement annulée. C'est une garantie d'intégrité fondamentale.
-- Tentons de vendre 10 armoires alors qu'il n'y en a que 5 en stock.
INSERT INTO vente (qte, id_produit) VALUES (10, 3);
-- ==> ERREUR: CHECK constraint 'produit_chk_1' is violated.
-- Vérifions les tables :
SELECT * FROM produit WHERE id_produit = 3; -- Le stock est toujours à 5.
SELECT * FROM vente WHERE id_produit = 3; -- La vente n'a pas été insérée.
-- La transaction a été annulée (ROLLBACK). L'état de la base est cohérent.
Chapitre 9 : Aller plus loin : Déclencheurs `BEFORE`
Jusqu'à présent, nous avons utilisé des triggers `AFTER`, qui s'exécutent après la modification des données. Mais il existe aussi les triggers `BEFORE`, qui s'exécutent **avant** que les données ne soient écrites dans la table. Ils sont parfaits pour deux usages principaux :
- Valider les données : On peut vérifier des conditions complexes et, si elles ne sont pas remplies, générer une erreur pour empêcher l'opération.
- Modifier les données à la volée : On peut nettoyer, formater ou calculer des valeurs avant qu'elles ne soient insérées. Par exemple, mettre un nom en majuscules, calculer un hash de mot de passe, etc.
Exemple : Formater un nom et valider un stock
Créons un trigger `BEFORE INSERT` sur notre table `produit` qui mettra automatiquement le nom du produit en majuscules et vérifiera que le stock initial n'est pas négatif (même si nous avons déjà un `CHECK`, cela montre le principe de validation).
DROP TRIGGER IF EXISTS before_insert_produit;
DELIMITER $$
CREATE TRIGGER before_insert_produit
BEFORE INSERT ON produit
FOR EACH ROW
BEGIN
-- 1. Modification des données : Mettre le nom en majuscules
SET NEW.nom = UPPER(NEW.nom);
-- 2. Validation des données : Empêcher un stock initial négatif
IF NEW.stock < 0 THEN
-- SIGNAL est la manière propre de générer une erreur personnalisée
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Le stock initial ne peut pas être négatif.';
END IF;
END$$
DELIMITER ;
-- Test 1: Insertion normale
INSERT INTO produit(nom, prix, stock) VALUES ('bureau', 800, 15);
SELECT * FROM produit WHERE id_produit = 4; -- Le nom est 'BUREAU'
-- Test 2: Insertion invalide
INSERT INTO produit(nom, prix, stock) VALUES ('lampe', 120, -5);
-- ==> ERREUR 1644 (45000): Le stock initial ne peut pas être négatif.
Ateliers Pratiques : Déclencheurs
Mettons en application ces concepts avec des cas d'usage réels pour automatiser la maintenance de nos bases de données.
Exercice 1 : Calcul automatique des heures de vol
Nous allons utiliser la base `vols`. Le but est de maintenir un compteur d'heures de vol pour chaque pilote, mis à jour automatiquement à chaque ajout, modification ou suppression de vol.
Exécutez ce code pour préparer la table `Pilote`.
USE vols;
-- Ajout du champ pour stocker le nombre d'heures de vol
ALTER TABLE pilote ADD COLUMN NBHV TIME DEFAULT '00:00:00';
-- Modification des colonnes de date pour permettre des calculs précis
ALTER TABLE vol MODIFY dated DATETIME;
ALTER TABLE vol MODIFY datea DATETIME;
Question 1 : Trigger après l'ajout d'un vol
DROP TRIGGER IF EXISTS after_insert_vol;
DELIMITER $$
CREATE TRIGGER after_insert_vol
AFTER INSERT ON vol
FOR EACH ROW
BEGIN
UPDATE pilote SET NBHV = ADDTIME(NBHV, TIMEDIFF(NEW.datea, NEW.dated))
WHERE numpilote = NEW.numpil;
END$$
DELIMITER ;
Question 2 : Trigger après la suppression d'un vol
DROP TRIGGER IF EXISTS after_delete_vol;
DELIMITER $$
CREATE TRIGGER after_delete_vol
AFTER DELETE ON vol
FOR EACH ROW
BEGIN
UPDATE pilote SET NBHV = SUBTIME(NBHV, TIMEDIFF(OLD.datea, OLD.dated))
WHERE numpilote = OLD.numpil;
END$$
DELIMITER ;
Question 3 : Trigger après la modification d'un vol
DROP TRIGGER IF EXISTS after_update_vol;
DELIMITER &&
CREATE TRIGGER after_update_vol
AFTER UPDATE ON vol
FOR EACH ROW
BEGIN
UPDATE pilote
SET NBHV = ADDTIME(
SUBTIME(NBHV, TIMEDIFF(OLD.datea, OLD.dated)),
TIMEDIFF(NEW.datea, NEW.dated)
)
WHERE numpilote = NEW.numpil;
END&&
DELIMITER ;
Exercice 2 : Calcul automatique du salaire moyen par département
Dans la base `employes`, nous voulons que le salaire moyen de chaque département soit toujours à jour. Pour cela, nous allons ajouter une colonne `salaire_moyen` à la table `DEPARTEMENT` et créer les triggers nécessaires sur la table `EMPLOYE`.
USE employes;
ALTER TABLE departement ADD salaire_moyen DECIMAL(8,2);
Question : Créer les triggers sur la table `EMPLOYE`
Note : MySQL requiert un trigger distinct pour chaque événement (`INSERT`, `UPDATE`, `DELETE`).
-- Trigger pour INSERT
DROP TRIGGER IF EXISTS after_insert_employe;
DELIMITER $$
CREATE TRIGGER after_insert_employe AFTER INSERT ON employe FOR EACH ROW
BEGIN
UPDATE departement SET salaire_moyen = (SELECT AVG(salaire) FROM employe WHERE id_dep = NEW.id_dep)
WHERE id_dep = NEW.id_dep;
END$$
DELIMITER ;
-- Trigger pour UPDATE
DROP TRIGGER IF EXISTS after_update_employe;
DELIMITER $$
CREATE TRIGGER after_update_employe AFTER UPDATE ON employe FOR EACH ROW
BEGIN
-- Mise à jour de l'ancien département si l'employé a changé de département
IF OLD.id_dep != NEW.id_dep THEN
UPDATE departement SET salaire_moyen = (SELECT AVG(salaire) FROM employe WHERE id_dep = OLD.id_dep)
WHERE id_dep = OLD.id_dep;
END IF;
-- Mise à jour du nouveau département
UPDATE departement SET salaire_moyen = (SELECT AVG(salaire) FROM employe WHERE id_dep = NEW.id_dep)
WHERE id_dep = NEW.id_dep;
END$$
DELIMITER ;
-- Trigger pour DELETE
DROP TRIGGER IF EXISTS after_delete_employe;
DELIMITER $$
CREATE TRIGGER after_delete_employe AFTER DELETE ON employe FOR EACH ROW
BEGIN
UPDATE departement SET salaire_moyen = (SELECT AVG(salaire) FROM employe WHERE id_dep = OLD.id_dep)
WHERE id_dep = OLD.id_dep;
END$$
DELIMITER ;
Exercice 3 : Calcul automatique du prix d'une recette
Dans la base `cuisine`, nous allons ajouter une colonne `prix` à la table `Recettes` et la maintenir à jour automatiquement lorsque la composition d'une recette change.
Bonne pratique : Utiliser une fonction d'aide
Le calcul du prix total d'une recette sera nécessaire dans plusieurs triggers. Pour éviter de dupliquer le code, nous allons d'abord créer une fonction qui s'en charge. C'est une pratique vivement recommandée.
USE cuisine;
ALTER TABLE recettes ADD COLUMN prix FLOAT DEFAULT 0;
DROP FUNCTION IF EXISTS get_recette_price;
DELIMITER $$
CREATE FUNCTION get_recette_price(r_id INT)
RETURNS FLOAT
READS SQL DATA
BEGIN
DECLARE cout FLOAT;
SELECT SUM(i.PUIng * cr.QteUtilisee) INTO cout
FROM ingredients i
JOIN composition_recette cr ON i.NumIng = cr.NumIng
WHERE cr.numrec = r_id;
RETURN IFNULL(cout, 0);
END$$
DELIMITER ;
Question : Créer les triggers sur la table `Composition_Recette`
-- Trigger pour INSERT
DROP TRIGGER IF EXISTS after_insert_composition;
DELIMITER $$
CREATE TRIGGER after_insert_composition AFTER INSERT ON composition_recette FOR EACH ROW
BEGIN
UPDATE recettes SET prix = get_recette_price(NEW.numrec) WHERE numrec = NEW.numrec;
END$$
DELIMITER ;
-- Trigger pour DELETE
DROP TRIGGER IF EXISTS after_delete_composition;
DELIMITER $$
CREATE TRIGGER after_delete_composition AFTER DELETE ON composition_recette FOR EACH ROW
BEGIN
UPDATE recettes SET prix = get_recette_price(OLD.numrec) WHERE numrec = OLD.numrec;
END$$
DELIMITER ;
-- Trigger pour UPDATE
DROP TRIGGER IF EXISTS after_update_composition;
DELIMITER $$
CREATE TRIGGER after_update_composition AFTER UPDATE ON composition_recette FOR EACH ROW
BEGIN
UPDATE recettes SET prix = get_recette_price(NEW.numrec) WHERE numrec = NEW.numrec;
END$$
DELIMITER ;
Pour aller plus loin : que se passe-t-il si le prix d'un ingrédient change ?
Vous avez raison de noter qu'un `UPDATE` sur la table `Ingredients` devrait aussi mettre à jour le prix des recettes. Créer un trigger `AFTER UPDATE ON Ingredients` est complexe car il faudrait trouver **toutes** les recettes qui utilisent cet ingrédient et les mettre à jour une par une. C'est un scénario où l'on doit parcourir un ensemble de résultats (la liste des recettes concernées) à l'intérieur d'un trigger, ce qui nous amène à des concepts plus avancés comme les **curseurs**, que nous aborderons dans une prochaine partie.
Partie 4 : Fiabilité et Robustesse : Transactions & Exceptions
Chapitre 10 : Garantir la Fiabilité : Gestion des Transactions
Dans de nombreuses applications, une opération métier n'est pas une seule requête SQL, mais un ensemble de plusieurs requêtes qui doivent toutes réussir ou toutes échouer ensemble. Un virement bancaire, par exemple, implique de débiter un compte ET de créditer un autre. Si l'une des deux actions échoue, l'autre doit être annulée. C'est le rôle des **transactions**.
Les Propriétés ACID
Une transaction garantit les propriétés ACID, qui sont le fondement des bases de données relationnelles fiables :
- Atomicité : La transaction est une unité de travail indivisible. Soit toutes les opérations réussissent, soit aucune n'est appliquée.
- Consistance : La transaction amène la base de données d'un état valide à un autre, en respectant toutes les règles et contraintes.
- Isolation : Les transactions concurrentes n'interfèrent pas les unes avec les autres. Tout se passe comme si elles étaient exécutées en série.
- Durabilité : Une fois qu'une transaction est validée (`COMMIT`), ses modifications sont permanentes, même en cas de panne système.
10.1. Les Commandes Clés
START TRANSACTION;: Démarre une nouvelle transaction. Toutes les instructions suivantes font partie de ce bloc.COMMIT;: Valide la transaction. Toutes les modifications effectuées depuis le `START TRANSACTION` deviennent permanentes.ROLLBACK;: Annule la transaction. Toutes les modifications effectuées sont annulées, et la base de données revient à l'état où elle était au début de la transaction.
10.2. Cas Pratique : Le Virement Bancaire
Voici l'exemple parfait d'une procédure qui doit être transactionnelle. Si la mise à jour du deuxième compte échoue (par exemple à cause d'une contrainte), la première mise à jour doit être impérativement annulée.
DROP DATABASE IF EXISTS bank;
CREATE DATABASE bank COLLATE utf8mb4_general_ci;
USE bank;
CREATE TABLE account (
account_number VARCHAR(50) PRIMARY KEY,
funds DECIMAL(8,2),
CHECK (funds >= 0)
);
INSERT INTO account VALUES (1, 10000), (2, 10000);
DROP PROCEDURE IF EXISTS transfert;
DELIMITER $$
CREATE PROCEDURE transfert(acc1 INT, acc2 INT, amount DOUBLE)
BEGIN
-- Si une erreur SQL se produit, le bloc BEGIN...END du handler est exécuté
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK; -- On annule toutes les modifications de la transaction
SELECT ("Opération annulée en raison d'une erreur.") AS 'Statut';
END;
START TRANSACTION;
UPDATE account SET funds = funds - amount WHERE account_number = acc1;
UPDATE account SET funds = funds + amount WHERE account_number = acc2;
COMMIT; -- Si tout s'est bien passé, on valide.
SELECT ("Transfert réussi.") AS 'Statut';
END$$
DELIMITER ;
-- Test 1: Transfert valide
CALL transfert(1, 2, 1000); -- Succès
SELECT * FROM account; -- Compte 1: 9000, Compte 2: 11000
-- Test 2: Transfert invalide (viole la contrainte CHECK funds >= 0)
CALL transfert(1, 2, 20000); -- Échec
SELECT * FROM account; -- Les fonds n'ont pas bougé. La transaction a été annulée.
Chapitre 11 : Maîtriser les Erreurs : Gestion des Exceptions
Par défaut, lorsqu'une erreur SQL se produit dans une procédure, son exécution s'arrête brutalement. La gestion des exceptions nous permet d'intercepter ces erreurs pour exécuter un code alternatif, comme annuler une transaction, enregistrer un log, ou renvoyer un message personnalisé, rendant nos procédures beaucoup plus robustes.
11.1. Le `HANDLER` : Intercepter les Erreurs
La déclaration `DECLARE HANDLER` permet de définir un "gestionnaire" pour un type d'erreur spécifique. On peut intercepter des erreurs très générales ou très spécifiques.
A. Gestion Générique (`SQLEXCEPTION`)
`SQLEXCEPTION` est un raccourci qui intercepte n'importe quelle erreur SQL.
USE commerce; -- Assurez-vous d'avoir créé cette base
DROP PROCEDURE IF EXISTS add_product_generic_handler;
DELIMITER $$
CREATE PROCEDURE add_product_generic_handler(name VARCHAR(100), price DOUBLE)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
SELECT "Une erreur d'insertion s'est produite." AS 'erreur';
END;
INSERT INTO produit (nom, prix) VALUES (name, price);
SELECT "Insertion effectuée avec succès." AS 'succes';
END$$
DELIMITER ;
CALL add_product_generic_handler('imprimante', -100);
B. Gestion par Code d'Erreur ou `SQLSTATE`
Pour une gestion plus fine, on peut cibler un code d'erreur MySQL (ex: `1062` pour `Duplicate entry`) ou un `SQLSTATE` (ex: `'23000'` pour une violation de contrainte d'unicité). L'utilisation de `SQLSTATE` est souvent préférée car elle est standardisée.
DROP PROCEDURE IF EXISTS add_product_specific_handler;
DELIMITER $$
CREATE PROCEDURE add_product_specific_handler(name VARCHAR(100), price DOUBLE)
BEGIN
DECLARE msg VARCHAR(100) DEFAULT '';
BEGIN
-- Handler pour la violation de la clé unique (code 1062)
DECLARE EXIT HANDLER FOR 1062 SET msg = "Ce produit existe déjà.";
-- Handler pour une colonne NOT NULL (code 1048)
DECLARE EXIT HANDLER FOR 1048 SET msg = "Le nom du produit ne peut pas être null.";
-- Handler pour une violation de contrainte CHECK (code 3819)
DECLARE EXIT HANDLER FOR 3819 SET msg = "Le prix du produit ne peut pas être négatif.";
INSERT INTO produit (nom, prix) VALUES (name, price);
END;
IF msg != '' THEN
SELECT msg AS 'erreur';
ELSE
SELECT "Insertion effectuée avec succès." AS 'succes';
END IF;
END$$
DELIMITER ;
CALL add_product_specific_handler('souris', 150); -- En supposant que 'souris' existe déjà
C. `GET DIAGNOSTICS` : Obtenir les détails de l'erreur
Pour créer des logs ou des messages d'erreur vraiment précis, `GET DIAGNOSTICS` permet de récupérer des informations sur l'erreur qui vient de se produire.
DROP PROCEDURE IF EXISTS add_product_with_diagnostics;
DELIMITER $$
CREATE PROCEDURE add_product_with_diagnostics(name VARCHAR(100), price DOUBLE)
BEGIN
DECLARE numero_erreur INT;
DECLARE message_original VARCHAR(255);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
numero_erreur = MYSQL_ERRNO,
message_original = MESSAGE_TEXT;
SELECT CONCAT("Erreur N°", numero_erreur, " : ", message_original) AS "Erreur détaillée";
END;
INSERT INTO produit (nom, prix) VALUES (name, price);
SELECT "Insertion réussie." AS "Succès";
END$$
DELIMITER ;
CALL add_product_with_diagnostics(NULL, 150);
11.2. `SIGNAL` : Déclencher ses Propres Erreurs
Parfois, une erreur n'est pas une erreur SQL, mais une violation d'une règle métier (ex: "un client VIP ne peut pas avoir un solde négatif"). `SIGNAL` nous permet de déclencher manuellement une erreur SQL, qui peut ensuite être interceptée par un `HANDLER`.
DROP PROCEDURE IF EXISTS diviser;
DELIMITER $$
CREATE PROCEDURE diviser(a INT, b INT, OUT r DOUBLE)
BEGIN
-- 1. On déclare une condition nommée pour notre erreur métier
DECLARE erreur_division CONDITION FOR SQLSTATE '45000';
-- 3. Le handler intercepte notre erreur personnalisée
DECLARE EXIT HANDLER FOR erreur_division
BEGIN
SELECT "Problème de division par zéro détecté." AS 'Erreur';
END;
IF b = 0 THEN
-- 2. On déclenche l'erreur si notre condition métier est violée
SIGNAL erreur_division SET MESSAGE_TEXT = 'Division par zéro interdite';
ELSE
SET r = a / b;
END IF;
END$$
DELIMITER ;
CALL diviser(10, 0, @r);
Ateliers Pratiques : Transactions et Exceptions
Il est temps de combiner ces deux concepts pour construire des procédures stockées véritablement robustes et sécurisées.
Exercice 1 : Transfert de chaises sécurisé
En utilisant la base de données `salles`, créer une procédure stockée qui gère le transfert de chaises d'une salle à une autre. L'opération doit être transactionnelle et échouer proprement si une des contraintes (nombre de chaises entre 20 et 30) est violée.
Script de mise en place de la base `salles` :
DROP DATABASE IF EXISTS salles;
CREATE DATABASE salles COLLATE utf8mb4_general_ci;
USE salles;
CREATE TABLE Salle (NumSalle INT AUTO_INCREMENT PRIMARY KEY, Etage INT, NombreChaises INT,
CONSTRAINT chk_NombreChaises CHECK (NombreChaises BETWEEN 20 AND 30));
CREATE TABLE Transfert (NumSalleOrigine INT, NumSalleDestination INT, DateTransfert DATE, NbChaisesTransferees INT,
CONSTRAINT fk_NumSalleOrigine FOREIGN KEY (NumSalleOrigine) REFERENCES salle(numsalle),
CONSTRAINT fk_NumSalleDestination FOREIGN KEY (NumSalleDestination) REFERENCES salle(numsalle));
INSERT INTO salle VALUES (1,1,24), (2,1,26), (3,1,26), (4,2,28);
DROP PROCEDURE IF EXISTS ps_transfert_chaises;
DELIMITER $$
CREATE PROCEDURE ps_transfert_chaises(salle_origine INT, salle_dest INT, nb_chaises INT)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SELECT "Impossible d’effectuer le transfert : une contrainte a été violée." AS 'message';
END;
START TRANSACTION;
UPDATE salle SET nombreChaises = nombreChaises - nb_chaises WHERE NumSalle = salle_origine;
UPDATE salle SET nombreChaises = nombreChaises + nb_chaises WHERE NumSalle = salle_dest;
INSERT INTO transfert VALUES (salle_origine, salle_dest, CURDATE(), nb_chaises);
COMMIT;
SELECT "Transfert effectué avec succès." AS 'message';
END$$
DELIMITER ;
-- Test 1 : Transfert valide (26-4=22 ; 26+4=30. OK)
CALL ps_transfert_chaises(2, 3, 4);
-- Test 2 : Transfert invalide (22-4=18. NON OK)
CALL ps_transfert_chaises(2, 3, 4);
Exercice 2 : Procédure d'insertion de produit sécurisée
Créer une procédure `ps_safe_add_product` pour la base `commerce`. Cette procédure doit accepter un nom et un prix, et retourner un message clair au format JSON indiquant le succès ou l'échec de l'opération, en gérant spécifiquement les erreurs de nom dupliqué, de nom `NULL` et de prix négatif.
USE commerce;
DROP PROCEDURE IF EXISTS ps_safe_add_product;
DELIMITER $$
CREATE PROCEDURE ps_safe_add_product(p_name VARCHAR(100), p_price DOUBLE)
BEGIN
DECLARE err_msg TEXT;
DECLARE err_code INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 err_code = MYSQL_ERRNO, err_msg = MESSAGE_TEXT;
SELECT JSON_OBJECT(
'statut', 'echec',
'erreur_code', err_code,
'message', err_msg
) AS 'resultat';
END;
INSERT INTO produit(nom, prix) VALUES (p_name, p_price);
SELECT JSON_OBJECT(
'statut', 'succes',
'id_produit_insere', LAST_INSERT_ID()
) AS 'resultat';
END$$
DELIMITER ;
-- Test 1: Succès
CALL ps_safe_add_product('Ecran 4K', 4500);
-- Test 2: Nom dupliqué
CALL ps_safe_add_product('Ecran 4K', 4600);
-- Test 3: Prix négatif
CALL ps_safe_add_product('Webcam', -50);
Exercice 3 : Virement bancaire ultra-sécurisé
Améliorer la procédure de virement bancaire pour la base `bank`. La procédure doit non seulement être transactionnelle, mais aussi vérifier en amont si les comptes existent et si les fonds sont suffisants. Elle doit retourner des messages d'erreur métier clairs et spécifiques pour chaque cas d'échec.
USE bank;
DROP PROCEDURE IF EXISTS secure_transfert;
DELIMITER $$
CREATE PROCEDURE secure_transfert(acc_from INT, acc_to INT, amount DECIMAL(8,2))
BEGIN
DECLARE funds_from DECIMAL(8,2);
DECLARE acc_from_exists, acc_to_exists INT;
-- Déclarer des conditions pour nos erreurs métier
DECLARE err_no_funds CONDITION FOR SQLSTATE '45000';
DECLARE err_acc_not_found CONDITION FOR SQLSTATE '45001';
DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SELECT 'Erreur SQL inattendue. Transaction annulée.' AS 'statut'; END;
START TRANSACTION;
-- Vérifier l'existence des comptes
SELECT COUNT(*) INTO acc_from_exists FROM account WHERE account_number = acc_from;
SELECT COUNT(*) INTO acc_to_exists FROM account WHERE account_number = acc_to;
IF acc_from_exists = 0 OR acc_to_exists = 0 THEN
SIGNAL err_acc_not_found SET MESSAGE_TEXT = 'Compte source ou destination introuvable.';
END IF;
-- Vérifier les fonds (en bloquant la ligne pour éviter les lectures concurrentes)
SELECT funds INTO funds_from FROM account WHERE account_number = acc_from FOR UPDATE;
IF funds_from < amount THEN
SIGNAL err_no_funds SET MESSAGE_TEXT = 'Fonds insuffisants sur le compte source.';
END IF;
-- Si tout est OK, on procède
UPDATE account SET funds = funds - amount WHERE account_number = acc_from;
UPDATE account SET funds = funds + amount WHERE account_number = acc_to;
COMMIT;
SELECT 'Transfert réussi.' AS 'statut';
END$$
DELIMITER ;
-- Test 1: Compte inexistant
CALL secure_transfert(1, 99, 100);
-- Test 2: Fonds insuffisants
CALL secure_transfert(1, 2, 50000);
-- Test 3: Succès
CALL secure_transfert(2, 1, 500);
Partie 5 : Itération sur les Données : Les Curseurs
Chapitre 12 : Le Traitement Ligne par Ligne : Les Curseurs
Jusqu'à présent, toutes nos opérations SQL fonctionnaient sur des **ensembles** de données. Un `UPDATE` modifie toutes les lignes qui correspondent à un critère, un `SELECT` retourne un jeu complet de résultats. Mais que faire si nous avons besoin de parcourir ce jeu de résultats, une ligne à la fois, pour effectuer un traitement complexe sur chacune d'entre elles ? C'est là qu'interviennent les **curseurs**.
Qu'est-ce qu'un curseur ?
Un curseur est un pointeur qui nous permet de "marcher" à travers le jeu de résultats d'une requête `SELECT`, ligne par ligne. Il nous donne la possibilité d'exécuter une logique procédurale (conditions, appels à d'autres procédures, etc.) pour chaque enregistrement individuel.
12.1. Le Cycle de Vie d'un Curseur
L'utilisation d'un curseur suit toujours quatre étapes précises :
- `DECLARE` : On déclare le curseur et on l'associe à une requête `SELECT`. À ce stade, la requête n'est pas encore exécutée.
- `OPEN` : On ouvre le curseur. La requête `SELECT` est exécutée, et les lignes du résultat sont stockées en mémoire, prêtes à être parcourues.
- `FETCH` : On récupère la ligne suivante pointée par le curseur et on stocke ses valeurs dans des variables locales. C'est cette étape que l'on place à l'intérieur d'une boucle.
- `CLOSE` : Une fois le traitement terminé, on ferme le curseur pour libérer les ressources allouées par la base de données.
12.2. Gérer la Fin de la Boucle
Comment savoir quand on a atteint la dernière ligne ? Le `FETCH` lève une condition `NOT FOUND` lorsqu'il n'y a plus de lignes à lire. Nous devons l'intercepter avec un `HANDLER` pour sortir proprement de notre boucle.
-- Déclaration d'une variable 'drapeau' pour contrôler la boucle
DECLARE flag BOOLEAN DEFAULT FALSE;
-- ... déclaration du curseur ...
-- Le handler qui se déclenchera quand FETCH ne trouvera plus de ligne
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
OPEN mon_curseur;
ma_boucle: LOOP
FETCH mon_curseur INTO var1, var2;
IF flag THEN
LEAVE ma_boucle;
END IF;
-- ... Traitement de la ligne en cours ...
END LOOP ma_boucle;
CLOSE mon_curseur;
Chapitre 13 : Curseurs en Pratique
Voyons un exemple concret. Nous souhaitons calculer le nombre total d'heures de vol pour chaque pilote et mettre à jour un champ `nbhv` dans la table `Pilote`. Sans curseur, il faudrait faire une requête par pilote. Avec un curseur, une seule procédure peut tout gérer.
Exemple : Calcul des heures de vol
USE vols;
ALTER TABLE pilote ADD nbhv TIME DEFAULT "00:00:00";
ALTER TABLE vol MODIFY datea DATETIME;
ALTER TABLE vol MODIFY dated DATETIME;
DROP PROCEDURE IF EXISTS set_nbhv;
DELIMITER $$
CREATE PROCEDURE set_nbhv()
BEGIN
DECLARE flag BOOLEAN DEFAULT FALSE;
DECLARE id_pilote_en_cours INT;
DECLARE temps_total TIME;
-- 1. DECLARE CURSOR
DECLARE c1 CURSOR FOR SELECT numpilote FROM pilote;
-- Déclaration du handler pour la fin du curseur
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
-- 2. OPEN CURSOR
OPEN c1;
boucle_pilotes: LOOP
-- 3. FETCH CURSOR
FETCH c1 INTO id_pilote_en_cours;
IF flag THEN
LEAVE boucle_pilotes;
END IF;
-- Traitement pour le pilote en cours
SELECT SEC_TO_TIME(SUM(TIME_TO_SEC(TIMEDIFF(datea, dated)))) INTO temps_total
FROM vol
WHERE numpil = id_pilote_en_cours;
UPDATE pilote SET nbhv = temps_total WHERE numpilote = id_pilote_en_cours;
END LOOP boucle_pilotes;
-- 4. CLOSE CURSOR
CLOSE c1;
END$$
DELIMITER ;
CALL set_nbhv();
Ateliers Pratiques : Curseurs
Les exercices suivants vous mettront au défi de manipuler des curseurs simples et imbriqués pour résoudre des problèmes qui seraient complexes à gérer avec du SQL standard.
Exercice 1 : Rapport complet des recettes
Reprendre la PS9 de la série sur les Procédures Stockées. Le but est de créer une procédure qui affiche une fiche détaillée pour **chaque** recette de la base `cuisine`, en utilisant un curseur pour itérer sur les recettes.
USE cuisine;
DROP PROCEDURE IF EXISTS Ps9_rapport_complet;
DELIMITER $$
CREATE PROCEDURE Ps9_rapport_complet()
BEGIN
DECLARE flag BOOLEAN DEFAULT FALSE;
DECLARE idRec INT;
DECLARE nameRec VARCHAR(50);
DECLARE methode VARCHAR(250);
DECLARE tpRec VARCHAR(20);
DECLARE prix DOUBLE;
DECLARE c CURSOR FOR SELECT numrec, nomrec, tempsPreparation, methodePreparation FROM recettes;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag=TRUE;
OPEN c;
l:LOOP
FETCH c INTO idRec, nameRec, tpRec, methode;
IF flag THEN
LEAVE l;
END IF;
SELECT CONCAT("Recette : ", nameRec, ", temps de préparation : ", tpRec, " min") AS "--- Message ---";
SELECT NomIng, QteUtilisee FROM composition_recette JOIN ingredients USING (NumIng) WHERE NumRec=idRec;
SELECT CONCAT("Sa méthode de préparation est : ", methode) AS "--- Méthode ---";
SELECT SUM(puIng * qteUtilisee) INTO prix FROM ingredients JOIN composition_recette USING(numIng) WHERE numRec=idRec;
IF prix < 50 THEN
SELECT CONCAT("Prix intéressant : ", FORMAT(prix, 2), " DH") AS "--- Avis Prix ---" ;
END IF;
END LOOP l;
CLOSE c;
END$$
DELIMITER ;
CALL Ps9_rapport_complet();
Exercice 2 : Trigger de mise à jour des prix avec curseur
C'est le moment de résoudre le problème laissé en suspens dans la partie sur les Triggers. Créer un trigger `AFTER UPDATE` sur la table `Ingredients` qui, lorsqu'un prix unitaire change, recalcule le prix de **toutes** les recettes qui utilisent cet ingrédient.
Logique
Quand le prix de l'ingrédient `N` change, le trigger doit :
1. Trouver toutes les recettes qui contiennent l'ingrédient `N`.
2. Utiliser un curseur pour parcourir cette liste de recettes.
3. Pour chaque recette, appeler la fonction `get_recette_price()` (créée précédemment) et mettre à jour son prix.
DROP TRIGGER IF EXISTS trg_after_update_ingredient_price;
DELIMITER //
CREATE TRIGGER trg_after_update_ingredient_price AFTER UPDATE ON Ingredients FOR EACH ROW
BEGIN
DECLARE id_recette_a_maj INT;
DECLARE flag BOOLEAN DEFAULT FALSE;
-- Le curseur sélectionne toutes les recettes affectées par le changement de prix
DECLARE c1 CURSOR FOR SELECT NumRec FROM composition_recette WHERE NumIng = NEW.NumIng;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
-- On exécute ce code seulement si le prix a réellement changé
IF OLD.PUIng != NEW.PUIng THEN
OPEN c1;
boucle_recettes: LOOP
FETCH c1 INTO id_recette_a_maj;
IF flag THEN
LEAVE boucle_recettes;
END IF;
UPDATE recettes SET prix = get_recette_price(id_recette_a_maj) WHERE numrec = id_recette_a_maj;
END LOOP boucle_recettes;
CLOSE c1;
END IF;
END//
DELIMITER ;
Exercice 3 : Rapport sur les pilotes et mise à jour de salaire
Cet exercice en plusieurs étapes utilise des curseurs imbriqués pour d'abord afficher un rapport, puis pour mettre à jour des données basées sur ce rapport.
Question 1 & 2 : Rapport détaillé des vols par pilote (curseurs imbriqués)
USE vols;
DROP PROCEDURE IF EXISTS rapport_vols_pilotes;
DELIMITER $$
CREATE PROCEDURE rapport_vols_pilotes()
BEGIN
DECLARE flag_p BOOLEAN DEFAULT FALSE;
DECLARE idpilote INT;
DECLARE nomP VARCHAR(50);
-- Curseur externe pour les pilotes
DECLARE c_pilotes CURSOR FOR SELECT numpilote, nom FROM pilote;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag_p = TRUE;
OPEN c_pilotes;
boucle_pilotes: LOOP
FETCH c_pilotes INTO idpilote, nomP;
IF flag_p THEN LEAVE boucle_pilotes; END IF;
SELECT CONCAT("--- Le pilote '", nomP, "' est affecté aux vols : ---") AS "Pilote";
-- Bloc interne pour le curseur des vols
BEGIN
DECLARE flag_v BOOLEAN DEFAULT FALSE;
DECLARE vd, va VARCHAR(50);
-- Curseur interne pour les vols du pilote en cours
DECLARE c_vols CURSOR FOR SELECT villed, villea FROM vol WHERE numpil = idpilote;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag_v = TRUE;
OPEN c_vols;
boucle_vols: LOOP
FETCH c_vols INTO vd, va;
IF flag_v THEN LEAVE boucle_vols; END IF;
SELECT CONCAT(" Départ : ", vd, " | Arrivée : ", va) AS "Vol";
END LOOP boucle_vols;
CLOSE c_vols;
END;
END LOOP boucle_pilotes;
CLOSE c_pilotes;
END$$
DELIMITER ;
CALL rapport_vols_pilotes();
Question 3 : Mise à jour du salaire en fonction du nombre de vols
Modifier la procédure précédente pour mettre à jour le salaire du pilote selon les règles suivantes : 0 vol = 5000; 1-3 vols = 7000; >3 vols = 8000.
ALTER TABLE pilote ADD salaire DOUBLE;
DROP PROCEDURE IF EXISTS maj_salaires_pilotes;
DELIMITER $$
CREATE PROCEDURE maj_salaires_pilotes()
BEGIN
DECLARE flag BOOLEAN DEFAULT FALSE;
DECLARE idpilote INT;
DECLARE salaire_actuel, salaire_nv DOUBLE;
DECLARE nb_vols INT;
DECLARE c CURSOR FOR SELECT numpilote, salaire FROM pilote;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
OPEN c;
b1: LOOP
FETCH c INTO idpilote, salaire_actuel;
IF flag THEN LEAVE b1; END IF;
SELECT COUNT(*) INTO nb_vols FROM vol WHERE numPil = idpilote;
CASE
WHEN nb_vols = 0 THEN SET salaire_nv = 5000;
WHEN nb_vols BETWEEN 1 AND 3 THEN SET salaire_nv = 7000;
ELSE SET salaire_nv = 8000;
END CASE;
UPDATE pilote SET salaire = salaire_nv WHERE numpilote = idpilote;
SELECT CONCAT("Pilote ID ", idpilote, ": Ancien salaire=", IFNULL(salaire_actuel, 0), ", Nouveau salaire=", salaire_nv) AS "Mise à jour";
END LOOP b1;
CLOSE c;
END$$
DELIMITER ;
CALL maj_salaires_pilotes();
Exercice 4 : Affecter les employés fatigués au groupe 'besoin vacances'
Pour la base `vacances`, créer une procédure avec un curseur qui parcourt la table `employe` et insère dans la table `groupe` tous les employés dont l'état est 'fatigué'.
DROP PROCEDURE IF EXISTS affecter_vacances;
DELIMITER $$
CREATE PROCEDURE affecter_vacances()
BEGIN
DECLARE flag BOOLEAN DEFAULT FALSE;
DECLARE mat INT;
DECLARE c CURSOR FOR SELECT matricule FROM employe WHERE etat = 'fatigué';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET flag = TRUE;
OPEN c;
b1: LOOP
FETCH c INTO mat;
IF flag THEN LEAVE b1; END IF;
INSERT INTO groupe VALUES (mat, 'besoin vacances');
END LOOP b1;
CLOSE c;
END$$
DELIMITER ;
CALL affecter_vacances();
Alternative sans curseur
Dans ce cas précis, un simple `INSERT ... SELECT` est beaucoup plus performant et lisible. C'est un excellent exemple de situation où il faut éviter d'utiliser un curseur.
INSERT INTO groupe (matricule, groupe)
SELECT matricule, 'besoin vacances'
FROM employe
WHERE etat = 'fatigué';
Partie 6 : Administration, Sécurité et Optimisation
Chapitre 14 : La Sécurité : Gestion des Utilisateurs et des Rôles
Une base de données contient souvent des informations sensibles. Assurer sa sécurité est une priorité absolue. Cela passe par une gestion rigoureuse des accès : qui peut se connecter, et une fois connecté, qui a le droit de voir ou de modifier quelles données. Le principe fondamental est celui du **moindre privilège** : un utilisateur ne doit avoir que les permissions strictement nécessaires à l'accomplissement de ses tâches.
14.1. Gestion des Utilisateurs
La première étape consiste à créer des comptes utilisateurs distincts. Chaque utilisateur est identifié par un nom et un hôte de connexion (par exemple, `'localhost'` pour les connexions locales, `'%'` pour n'importe quel hôte).
-- Supprimer un utilisateur s'il existe déjà pour éviter une erreur
DROP USER IF EXISTS 'yousra'@'localhost';
-- Créer un nouvel utilisateur avec son mot de passe
CREATE USER 'yousra'@'localhost' IDENTIFIED BY '123456';
-- Modifier le mot de passe d'un utilisateur existant
SET PASSWORD FOR 'yousra'@'localhost' = "abcdefg";
14.2. Attribution des Privilèges (`GRANT`)
Une fois l'utilisateur créé, il n'a aucun droit. La commande `GRANT` permet de lui accorder des permissions spécifiques sur des objets de la base de données.
-- Attribuer TOUS les droits sur TOUS les objets de la base 'cuisine'
GRANT ALL PRIVILEGES ON cuisine.* TO 'yousra'@'localhost';
-- Attribuer des droits spécifiques (SELECT, INSERT, UPDATE) sur une table précise
GRANT SELECT, INSERT, UPDATE ON vols.avion TO 'yousra'@'localhost';
-- Attribuer des droits sur des colonnes spécifiques
-- L'utilisateur ne pourra voir que les villes de départ et d'arrivée
GRANT SELECT(villed, villea) ON vols.vol TO 'yousra'@'localhost';
-- L'utilisateur ne pourra modifier que ces deux colonnes
GRANT UPDATE(villed, villea) ON vols.vol TO 'yousra'@'localhost';
14.3. Révocation des Privilèges (`REVOKE`)
La commande `REVOKE` est le contraire de `GRANT`. Elle permet de retirer des permissions à un utilisateur.
-- Pour voir les droits actuels d'un utilisateur
SHOW GRANTS FOR 'yousra'@'localhost';
-- Enlever les droits d'insertion et de modification sur la table 'avion'
REVOKE INSERT, UPDATE ON vols.avion FROM 'yousra'@'localhost';
-- Vérifier que les droits ont bien été retirés
SHOW GRANTS FOR 'yousra'@'localhost';
14.4. Simplifier la Gestion avec les Rôles
Quand on gère des dizaines d'utilisateurs, attribuer les droits un par un devient fastidieux. Les **rôles** permettent de créer des profils de permissions (ex: 'lecteur', 'editeur', 'administrateur'), d'attribuer les droits à ces rôles, puis d'attribuer simplement les rôles aux utilisateurs. C'est beaucoup plus simple à maintenir.
-- 1. Créer les rôles
CREATE ROLE IF NOT EXISTS 'etudiants'@'localhost', 'profs'@'localhost';
-- 2. Donner des droits aux rôles
GRANT SELECT ON vols.vol TO 'etudiants'@'localhost';
GRANT ALL PRIVILEGES ON vols.* TO 'profs'@'localhost';
-- 3. Créer des utilisateurs
CREATE USER IF NOT EXISTS 'u1'@'localhost' IDENTIFIED BY '123';
CREATE USER IF NOT EXISTS 'u4'@'localhost' IDENTIFIED BY '123';
-- 4. Affecter les rôles aux utilisateurs
GRANT 'etudiants'@'localhost' TO 'u1'@'localhost';
GRANT 'etudiants'@'localhost', 'profs'@'localhost' TO 'u4'@'localhost';
-- 5. Activer les rôles par défaut pour les utilisateurs
SET DEFAULT ROLE ALL TO 'u1'@'localhost', 'u4'@'localhost';
-- Vérifier les droits de u4 (il a ses propres droits + ceux des deux rôles)
SHOW GRANTS FOR 'u4'@'localhost';
Chapitre 15 : Objets de Base de Données Virtuels : Vues et Tables Temporaires
En plus des tables physiques, MySQL propose des structures de données "virtuelles" ou éphémères qui sont extrêmement utiles pour simplifier le code et organiser les traitements de données complexes.
15.1. Les Vues (`VIEW`)
Une **vue** est une requête `SELECT` qui est stockée dans la base de données et qui se comporte comme une table virtuelle. C'est un outil puissant pour :
- Simplifier la complexité : Une jointure complexe ou un calcul répétitif peut être masqué derrière une vue simple à interroger.
- Sécuriser les données : On peut créer une vue qui n'expose que certaines lignes ou colonnes d'une table, et donner aux utilisateurs l'accès à la vue plutôt qu'à la table complète.
USE vols;
-- On crée une vue qui représente une jointure complexe
CREATE OR REPLACE VIEW v_details_vols AS
SELECT
v.numvol, v.villed, v.villea,
p.nom AS 'nom_pilote',
a.typeav AS 'type_avion'
FROM vol v
JOIN pilote p ON v.numpil = p.numpilote
JOIN avion a ON v.numav = a.numav;
-- Maintenant, on peut interroger la vue comme une simple table
SELECT * FROM v_details_vols WHERE nom_pilote = 'hassan';
15.2. Les Tables Temporaires (`TEMPORARY TABLE`)
Une **table temporaire** est une table qui n'existe que pour la durée de la session de connexion en cours. Elle est automatiquement détruite à la déconnexion. Elles sont idéales pour stocker des résultats intermédiaires lors d'une analyse de données en plusieurs étapes.
-- Créer une table temporaire vide avec une structure définie
CREATE TEMPORARY TABLE tva (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50),
valeur DOUBLE
);
INSERT INTO tva (nom, valeur) VALUES ('normal', 0.20), ('reduit', 0.07);
-- Créer une table temporaire à partir du résultat d'un SELECT
CREATE TEMPORARY TABLE volsOfCasa AS
SELECT * FROM vol WHERE villed = 'casablanca';
SELECT COUNT(*) FROM volsOfCasa;
-- Si vous fermez votre connexion à MySQL et la rouvrez, la table 'volsOfCasa' n'existera plus.
15.3. Les Tables en Mémoire (`ENGINE=MEMORY`)
Une autre forme de table non persistante est la table en mémoire, créée avec le moteur de stockage MEMORY. Contrairement à une table temporaire qui est liée à une session, une table en mémoire est visible par toutes les sessions connectées au serveur MySQL, mais son contenu est entièrement stocké dans la RAM.
Cela les rend extrêmement rapides, idéales pour des données de référence ou des caches qui doivent être accessibles rapidement et par plusieurs utilisateurs. Cependant, leur contenu est volatile : si le serveur MySQL redémarre ou s'arrête, les données de ces tables sont perdues.
Les principales utilisations sont :
- Mise en cache rapide : Pour stocker des résultats de requêtes complexes ou des données rarement modifiées afin d'accélérer les lectures.
-- Créer une table de cache en mémoire pour des paramètres d'application
CREATE TABLE app_cache (
param_name VARCHAR(100) PRIMARY KEY,
param_value VARCHAR(255)
) ENGINE=MEMORY;
-- On peut l'alimenter comme n'importe quelle autre table
INSERT INTO app_cache (param_name, param_value) VALUES
('site_name', 'Mon Super Site'),
('max_connections', '100');
-- La lecture des données est très rapide
SELECT param_value FROM app_cache WHERE param_name = 'site_name';
-- Si le serveur MySQL redémarre, la table 'app_cache' sera vide.
-- Pour la supprimer manuellement, on utilise DROP TABLE.
DROP TABLE app_cache;
Chapitre 16 : Sauvegarde et Restauration (`mysqldump`)
La sauvegarde régulière des données est la tâche la plus critique de l'administration de bases de données. Elle protège contre la perte de données due à une défaillance matérielle, une erreur humaine, ou une attaque malveillante. L'outil standard pour effectuer des sauvegardes logiques (qui génèrent un fichier `.sql`) est `mysqldump`.
16.1. Sauvegarder une base de données (`mysqldump`)
La commande `mysqldump` s'exécute depuis la ligne de commande (Terminal, PowerShell, CMD), et non depuis le client MySQL. Vous devez vous placer dans le répertoire `bin` de votre installation MySQL ou l'ajouter à votre `PATH` système.
# Syntaxe de base : sauvegarder la base 'cuisine' dans un fichier .sql
# L'option -p sans mot de passe vous demandera le mot de passe de manière sécurisée.
mysqldump -u root -p cuisine > sauvegarde_cuisine.sql
# Spécifier l'hôte (-h) et le port (-P, majuscule) si ce ne sont pas les valeurs par défaut
mysqldump -h 127.0.0.1 -P 3306 -u root -p cuisine > sauvegarde_cuisine_host.sql
# Sauvegarder plusieurs bases de données en même temps
mysqldump -u root -p --databases vols cuisine > sauvegarde_multiples.sql
# Sauvegarder TOUTES les bases de données du serveur
mysqldump -u root -p --all-databases > sauvegarde_totale_serveur.sql
16.2. Restaurer une base de données
Pour restaurer une sauvegarde, il faut d'abord créer une base de données vide pour accueillir les données, puis importer le fichier `.sql`.
Méthode 1 : Ligne de commande (recommandée pour les gros fichiers)
# 1. Se connecter à MySQL et créer la base de données de destination
mysql -u root -p
CREATE DATABASE restauration_test;
EXIT;
# 2. Importer le fichier .sql dans la nouvelle base de données
mysql -u root -p restauration_test < sauvegarde_cuisine.sql
Méthode 2 : Commande `SOURCE` dans le client MySQL
-- 1. Se connecter à MySQL
mysql -u root -p
-- 2. Créer et utiliser la base de données
CREATE DATABASE restauration_test2;
USE restauration_test2;
-- 3. Exécuter la commande SOURCE (préciser le chemin complet si nécessaire)
SOURCE C:/chemin/vers/sauvegarde_cuisine.sql;
Ateliers Pratiques : Administration et Sécurité
Appliquons ces concepts d'administration pour gérer les accès et optimiser les requêtes sur nos bases de données.
Exercice 1 : Gestion des Rôles pour un Restaurant
Pour la base `cuisine`, vous devez mettre en place une politique de sécurité basée sur les rôles pour trois types d'employés : 'Gestionnaire', 'Cuisinier' et 'Stagiaire'.
- Gestionnaire : Doit avoir tous les droits sur toute la base de données.
- Cuisinier : Doit pouvoir tout lire (`SELECT`) sur toutes les tables, mais ne peut modifier (`INSERT`, `UPDATE`) que les tables `Recettes` et `Composition_Recette`.
- Stagiaire : Ne peut que lire (`SELECT`) la table `Ingredients` et la table `Recettes`.
Créez les rôles, assignez-leur les bons privilèges, puis créez un utilisateur pour chaque profil et assignez-leur le rôle approprié.
-- 1. Création des rôles
CREATE ROLE IF NOT EXISTS
'gestionnaire_cuisine'@'localhost',
'cuisinier'@'localhost',
'stagiaire_cuisine'@'localhost';
-- 2. Attribution des privilèges aux rôles
GRANT ALL PRIVILEGES ON cuisine.* TO 'gestionnaire_cuisine'@'localhost';
GRANT SELECT ON cuisine.* TO 'cuisinier'@'localhost';
GRANT INSERT, UPDATE ON cuisine.Recettes TO 'cuisinier'@'localhost';
GRANT INSERT, UPDATE ON cuisine.Composition_Recette TO 'cuisinier'@'localhost';
GRANT SELECT ON cuisine.Ingredients TO 'stagiaire_cuisine'@'localhost';
GRANT SELECT ON cuisine.Recettes TO 'stagiaire_cuisine'@'localhost';
-- 3. Création des utilisateurs
CREATE USER IF NOT EXISTS 'ali_gestionnaire'@'localhost' IDENTIFIED BY 'pass1';
CREATE USER IF NOT EXISTS 'fatima_cuisiniere'@'localhost' IDENTIFIED BY 'pass2';
CREATE USER IF NOT EXISTS 'karim_stagiaire'@'localhost' IDENTIFIED BY 'pass3';
-- 4. Attribution des rôles aux utilisateurs
GRANT 'gestionnaire_cuisine'@'localhost' TO 'ali_gestionnaire'@'localhost';
GRANT 'cuisinier'@'localhost' TO 'fatima_cuisiniere'@'localhost';
GRANT 'stagiaire_cuisine'@'localhost' TO 'karim_stagiaire'@'localhost';
-- 5. Activation des rôles
SET DEFAULT ROLE ALL TO
'ali_gestionnaire'@'localhost',
'fatima_cuisiniere'@'localhost',
'karim_stagiaire'@'localhost';
FLUSH PRIVILEGES;
Exercice 2 : Vues de Synthèse pour l'Aéroport
Pour la base `vols`, créez deux vues pour simplifier le reporting :
- `v_vols_boeing` : Une vue qui n'affiche que les informations des vols (numéro de vol, villes départ/arrivée, nom du pilote) effectués avec un avion de type 'boeing'.
- `v_pilotes_experimentes` : Une vue qui liste les pilotes (nom, ville, date de début) ayant commencé à travailler avant le 1er janvier 2010.
USE vols;
-- 1. Vue pour les vols en Boeing
CREATE OR REPLACE VIEW v_vols_boeing AS
SELECT v.numvol, v.villed, v.villea, p.nom AS 'nom_pilote'
FROM vol v
JOIN avion a ON v.numav = a.numav
JOIN pilote p ON v.numpil = p.numpilote
WHERE a.typeav = 'boeing';
-- Utilisation de la vue
SELECT * FROM v_vols_boeing;
-- 2. Vue pour les pilotes expérimentés
CREATE OR REPLACE VIEW v_pilotes_experimentes AS
SELECT nom, villepilote, datedebut
FROM pilote
WHERE datedebut < '2010-01-01';
-- Utilisation de la vue
SELECT * FROM v_pilotes_experimentes;
Conclusion & Perspectives
Félicitations pour avoir terminé ce parcours intensif sur MySQL !
Vous avez maintenant les clés pour dépasser le simple rôle de consommateur de données et devenir un véritable architecte de la logique applicative côté serveur. De la création de fonctions réutilisables à l'automatisation avec les triggers, en passant par la garantie de l'intégrité avec les transactions et la manipulation fine avec les curseurs, vous disposez d'une boîte à outils puissante pour construire des systèmes de données performants, robustes et sécurisés. Continuez à pratiquer, car c'est en forgeant que l'on devient forgeron !