Logo OFPPT

ISMO - Institut Spécialisé dans les Métiers de l'Offshoring

OFPPT - Office de la Formation Profesionnelle et de la Promotion du Travail

Photo de F. Rahmouni Oussama

Par F. Rahmouni Oussama

Formateur en Développement Informatique & Data Science, ISMO

Maîtrise du SQL Procédural avec MySQL

Dernière mise à jour : Octobre 2025

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 :

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 :

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.

Attention à la performance : Les curseurs sont puissants mais doivent être utilisés avec parcimonie. Le traitement ligne par ligne est intrinsèquement plus lent qu'une opération SQL basée sur un ensemble. Privilégiez toujours une solution SQL pure si elle existe.

12.1. Le Cycle de Vie d'un Curseur

L'utilisation d'un curseur suit toujours quatre étapes précises :

  1. `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.
  2. `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.
  3. `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.
  4. `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 :

  1. `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'.
  2. `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 !