Structurer ses instructions

Lorsque l'on écrit une série d'instructions, par exemple dans le corps d'une procédure stockée, il est nécessaire d'être capable de structurer ses instructions. Cela va permettre d’instiller de la logique dans le traitement : exécuter telles ou telles instructions en fonction des données que l'on possède, répéter une instruction un certain nombre de fois, etc.

Voici quelques outils indispensables à la structuration des instructions :

  • les variables locales : qui vont permettre de stocker et modifier des valeurs pendant le déroulement d'une procédure ;
  • les conditions : qui vont permettre d'exécuter certaines instructions seulement si une certaine condition est remplie ;
  • les boucles : qui vont permettre de répéter une instruction plusieurs fois.

Ces structures sont bien sûr utilisables dans les procédures stockées, que nous avons vues au chapitre précédent, mais pas uniquement. Elles sont utilisables dans tout objet définissant une série d'instructions à exécuter. C'est le cas des fonctions stockées (non couvertes par ce cours et qui forment avec les procédures stockées ce qu'on appelle les "routines"), des événements (non couverts), et également des triggers, auxquels un chapitre est consacré à la fin de cette partie.

Blocs d'instructions et variables locales

Blocs d'instructions

Nous avons vu qu'un bloc d'instructions était défini par les mots-clés BEGIN et END, entre lesquels on met les instructions qui composent le bloc (de zéro à autant d'instructions que l'on veut, séparées bien sûr d'un ;).

Il est possible d'imbriquer plusieurs blocs d'instructions. De même, à l'intérieur d'un bloc d'instructions, plusieurs blocs d'instructions peuvent se suivre. Ceux-ci permettent donc de structurer les instructions en plusieurs parties distinctes et sur plusieurs niveaux d'imbrication différents.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
BEGIN
    SELECT 'Bloc d''instructions principal';

    BEGIN
        SELECT 'Bloc d''instructions 2, imbriqué dans le bloc principal';

        BEGIN
            SELECT 'Bloc d''instructions 3, imbriqué dans le bloc d''instructions 2';
        END;
    END;

    BEGIN
        SELECT 'Bloc d''instructions 4, imbriqué dans le bloc principal';
    END;

END;

Cet exemple montre également l'importance de l'indentation pour avoir un code lisible. Ici, toutes les instructions d'un bloc sont au même niveau et décalées vers la droite par rapport à la déclaration du bloc. Cela permet de voir en un coup d’œil où commence et où se termine chaque bloc d'instructions.

Variables locales

Nous connaissons déjà les variables utilisateur, qui sont des variables désignées par @. J'ai également mentionné l'existence des variables système, qui sont des variables prédéfinies par MySQL. Voyons maintenant les variables locales, qui peuvent être définies dans un bloc d'instructions.

Déclaration d'une variable locale

La déclaration d'une variable locale se fait avec l'instruction DECLARE :

1
DECLARE nom_variable type_variable [DEFAULT valeur_defaut];

Cette instruction doit se trouver au tout début du bloc d'instructions dans lequel la variable locale sera utilisée (donc directement après le BEGIN).

On a donc une structure générale des blocs d'instructions qui se dégage :

1
2
3
4
5
BEGIN
    -- Déclarations (de variables locales par exemple)

    -- Instructions (dont éventuels blocs d'instructions imbriqués)
END;

Tout comme pour les variables utilisateur, le nom des variables locales n'est pas sensible à la casse.

Si aucune valeur par défaut n'est précisée, la variable vaudra NULL tant que sa valeur n'est pas changée. Pour changer la valeur d'une variable locale, on peut utiliser SET ou SELECT ... INTO.

Exemple : voici une procédure stockée qui donne la date d'aujourd'hui et de demain :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
DELIMITER |
CREATE PROCEDURE aujourdhui_demain ()
BEGIN
    DECLARE v_date DATE DEFAULT CURRENT_DATE();               -- On déclare une variable locale et on lui met une valeur par défaut

    SELECT DATE_FORMAT(v_date, '%W %e %M %Y') AS Aujourdhui;

    SET v_date = v_date + INTERVAL 1 DAY;                     -- On change la valeur de la variable locale
    SELECT DATE_FORMAT(v_date, '%W %e %M %Y') AS Demain;
END|
DELIMITER ;

Testons-la :

1
2
SET lc_time_names = 'fr_FR';
CALL aujourdhui_demain();

Aujourdhui

mardi 1 mai 2012

Demain

mercredi 2 mai 2012

Tout comme pour les paramètres, les variables locales peuvent poser problème si l'on ne fait pas attention au nom qu'on leur donne. En cas de conflit (avec un nom de colonne par exemple), comme pour les paramètres, le nom sera interprété comme désignant la variable locale en priorité. Par conséquent, toutes mes variables locales seront préfixées par "v_".

Portée des variables locales dans un bloc d'instruction

Les variables locales n'existent que dans le bloc d'instructions dans lequel elles ont été déclarées. Dès que le mot-clé END est atteint, toutes les variables locales du bloc sont détruites.

Exemple 1 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
DELIMITER |
CREATE PROCEDURE test_portee1()
BEGIN
    DECLARE v_test1 INT DEFAULT 1;

    BEGIN
        DECLARE v_test2 INT DEFAULT 2;

        SELECT 'Imbriqué' AS Bloc;
        SELECT v_test1, v_test2;
    END;
    SELECT 'Principal' AS Bloc;
    SELECT v_test1, v_test2;

END|
DELIMITER ;

CALL test_portee1();

Bloc

Imbriqué

v_test1

v_test2

1

2

Bloc

Principal

1
ERROR 1054 (42S22): Unknown column 'v_test2' in 'field list'

La variable locale v_test2 existe bien dans le bloc imbriqué, puisque c'est là qu'elle est définie, mais pas dans le bloc principal. v_test1 par contre existe dans le bloc principal (où elle est définie), mais aussi dans le bloc imbriqué.

Exemple 2 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER |
CREATE PROCEDURE test_portee2()
BEGIN
    DECLARE v_test1 INT DEFAULT 1;

    BEGIN
        DECLARE v_test2 INT DEFAULT 2;

        SELECT 'Imbriqué 1' AS Bloc;
        SELECT v_test1, v_test2;
    END;

    BEGIN
        SELECT 'imbriqué 2' AS Bloc;
        SELECT v_test1, v_test2;    
    END;


END|
DELIMITER ;

CALL test_portee2();

Bloc

Imbriqué 1

v_test1

v_test2

1

2

Bloc

imbriqué 2

1
ERROR 1054 (42S22): Unknown column 'v_test2' in 'field list'

À nouveau, v_test1, déclarée dans le bloc principal, existe dans les deux blocs imbriqués. Par contre, v_test2 n'existe que dans le bloc imbriqué dans lequel elle est déclarée.

Attention cependant à la subtilité suivante : si un bloc imbriqué déclare une variable locale ayant le même nom qu'une variable locale déclarée dans un bloc d'un niveau supérieur, il s'agira toujours de deux variables locales différentes, et seule la variable locale déclarée dans le bloc imbriqué sera visible dans ce même bloc.

Exemple 3 :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER |
CREATE PROCEDURE test_portee3()
BEGIN
    DECLARE v_test INT DEFAULT 1;

    SELECT v_test AS 'Bloc principal';

    BEGIN
        DECLARE v_test INT DEFAULT 0;

        SELECT v_test AS 'Bloc imbriqué';
        SET v_test = 2;
        SELECT v_test AS 'Bloc imbriqué après modification';
    END;

    SELECT v_test AS 'Bloc principal';
END |
DELIMITER ;

CALL test_portee3();

Bloc principal

1

Bloc imbriqué

0

Bloc imbriqué après modification

2

Bloc principal

1

La variable locale v_test est déclarée dans le bloc principal et dans le bloc imbriqué, avec deux valeurs différentes. Mais lorsqu'on revient dans le bloc principal après exécution du bloc d'instructions imbriqué, v_test a toujours la valeur qu'elle avait avant l'exécution de ce bloc et sa deuxième déclaration. Il s'agit donc bien de deux variables locales distinctes.

Structures conditionnelles

Les structures conditionnelles permettent de déclencher une action ou une série d'instructions lorsqu'une condition préalable est remplie.

MySQL propose deux structures conditionnelles : IF et CASE.

La structure IF

Voici la syntaxe de la structure IF :

1
2
3
4
5
IF condition THEN instructions
[ELSEIF autre_condition THEN instructions
[ELSEIF ...]]
[ELSE instructions]
END IF;

Le cas le plus simple : si la condition est vraie, alors on exécute ces instructions

Voici la structure minimale d'un IF :

1
2
3
IF condition THEN
    instructions
END IF;

Soit on exécute les instructions (si la condition est vraie), soit on ne les exécute pas.

Exemple : la procédure suivante affiche 'J''ai déjà été adopté !', si c'est le cas, à partir de l'id d'un animal :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
DELIMITER |
CREATE PROCEDURE est_adopte(IN p_animal_id INT)
BEGIN
    DECLARE v_nb INT DEFAULT 0;           -- On crée une variable locale

    SELECT COUNT(*) INTO v_nb             -- On met le nombre de lignes correspondant à l'animal 
    FROM Adoption                         --  dans Adoption dans notre variable locale
    WHERE animal_id = p_animal_id;

    IF v_nb > 0 THEN                      -- On teste si v_nb est supérieur à 0 (donc si l'animal a été adopté)
        SELECT 'J''ai déjà été adopté !';
    END IF;                               -- Et on n'oublie surtout pas le END IF et le ; final
END |
DELIMITER ;

CALL est_adopte(3);
CALL est_adopte(28);

Seul le premier appel à la procédure va afficher 'J''ai déjà été adopté !', puisque l'animal 3 est présent dans la table Adoption, contrairement à l'animal 28.

Deuxième cas : si … alors, sinon …

Grâce au mot-clé ELSE, on peut définir une série d'instructions à exécuter si la condition est fausse.

ELSE ne doit pas être suivi de THEN.

Exemple : la procédure suivante affiche 'Je suis né avant 2010' ou 'Je suis né après 2010', selon la date de naissance de l'animal transmis en paramètre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
DELIMITER |
CREATE PROCEDURE avant_apres_2010(IN p_animal_id INT)
BEGIN
    DECLARE v_annee INT;

    SELECT YEAR(date_naissance) INTO v_annee
    FROM Animal
    WHERE id = p_animal_id;

    IF v_annee < 2010 THEN
        SELECT 'Je suis né avant 2010' AS naissance;
    ELSE                                               -- Pas de THEN
        SELECT 'Je suis né après 2010' AS naissance;
    END IF;                                            -- Toujours obligatoire

END |
DELIMITER ;

CALL avant_apres_2010(34);   -- Né le 20/04/2008
CALL avant_apres_2010(69);   -- Né le 13/02/2012

Troisième et dernier cas : plusieurs conditions alternatives

Enfin, le mot-clé ELSEIF... THEN permet de vérifier d'autres conditions (en dehors de la condition du IF), chacune ayant une série d'instructions définies à exécuter en cas de véracité. Si plusieurs conditions sont vraies en même temps, seule la première rencontrée verra ses instructions exécutées. On peut bien sûr toujours (mais ce n'est pas obligatoire) ajouter un ELSE pour le cas où aucune condition ne serait vérifiée.

Exemple : cette procédure affiche un message différent selon le sexe de l'animal passé en paramètre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER |
CREATE PROCEDURE message_sexe(IN p_animal_id INT)
BEGIN
    DECLARE v_sexe VARCHAR(10);

    SELECT sexe INTO v_sexe
    FROM Animal
    WHERE id = p_animal_id;

    IF (v_sexe = 'F') THEN                   -- Première possibilité
        SELECT 'Je suis une femelle !' AS sexe;
    ELSEIF (v_sexe = 'M') THEN                  -- Deuxième possibilité
        SELECT 'Je suis un mâle !' AS sexe;
    ELSE                                           -- Défaut
        SELECT 'Je suis en plein questionnement existentiel...' AS sexe;
    END IF;
END|
DELIMITER ;

CALL message_sexe(8);   -- Mâle
CALL message_sexe(6);   -- Femelle
CALL message_sexe(9);   -- Ni l'un ni l'autre

Il peut bien sûr y avoir autant de ELSEIF... THEN que l'on veut (mais un seul ELSE).

La structure CASE

Deux syntaxes sont possibles pour utiliser CASE.

Première syntaxe : conditions d'égalité

1
2
3
4
5
CASE valeur_a_comparer
    WHEN possibilite1 THEN instructions
    [WHEN possibilite2 THEN instructions] ...
    [ELSE instructions]
END CASE;

Exemple : on reprend la procédure message_sexe(), et on l'adapte pour utiliser CASE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DELIMITER |
CREATE PROCEDURE message_sexe2(IN p_animal_id INT)
BEGIN
    DECLARE v_sexe VARCHAR(10);

    SELECT sexe INTO v_sexe
    FROM Animal
    WHERE id = p_animal_id;

    CASE v_sexe
        WHEN 'F' THEN                          -- Première possibilité
            SELECT 'Je suis une femelle !' AS sexe;
        WHEN 'M' THEN                             -- Deuxième possibilité
            SELECT 'Je suis un mâle !' AS sexe;
        ELSE                                         -- Défaut
            SELECT 'Je suis en plein questionnement existentiel...' AS sexe;
    END CASE;
END|
DELIMITER ;

CALL message_sexe2(8);   -- Mâle
CALL message_sexe2(6);   -- Femelle
CALL message_sexe2(9);   -- Ni l'un ni l'autre

On définit donc v_sexe comme point de comparaison. Chaque WHEN donne alors un élément auquel v_sexe doit être comparé. Les instructions exécutées seront celles du WHEN dont l'élément est égal à v_sexe. Le ELSE sera exécuté si aucun WHEN ne correspond.

Ici, on compare une variable locale (v_sexe) à des chaînes de caractères ('F' et 'M'), mais on peut utiliser différents types d'éléments. Voici les principaux :

  • des variables locales ;
  • des variables utilisateur ;
  • des valeurs constantes de tous types (0, 'chaîne', 5.67, '2012-03-23',…) ;
  • des expressions (2 + 4, NOW(), CONCAT(nom, ' ', prenom),…) ;

Cette syntaxe ne permet pas de faire des comparaisons avec NULL, puisqu'elle utilise une comparaison de type valeur1 = valeur2. Or cette comparaison est inutilisable dans le cas de NULL. Il faudra donc utiliser la seconde syntaxe, avec le test IS NULL.

Seconde syntaxe : toutes conditions

Cette seconde syntaxe ne compare pas un élément à différentes valeurs, mais utilise simplement des conditions classiques et permet donc de faire des comparaisons de type "plus grand que", "différent de", etc. (bien entendu, elle peut également être utilisée pour des égalités).

1
2
3
4
5
CASE
    WHEN condition THEN instructions
    [WHEN condition THEN instructions] ...
    [ELSE instructions]
END CASE

Exemple : on reprend la procédure avant_apres_2010(), qu'on réécrit avec CASE, et en donnant une possibilité en plus. De plus, on passe le message en paramètre OUT pour changer un peu.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DELIMITER |
CREATE PROCEDURE avant_apres_2010_case (IN p_animal_id INT, OUT p_message VARCHAR(100))
BEGIN
    DECLARE v_annee INT;

    SELECT YEAR(date_naissance) INTO v_annee
    FROM Animal
    WHERE id = p_animal_id;

    CASE
        WHEN v_annee < 2010 THEN
            SET p_message = 'Je suis né avant 2010.';
        WHEN v_annee = 2010 THEN
            SET p_message = 'Je suis né en 2010.';
        ELSE
            SET p_message = 'Je suis né après 2010.';   
    END CASE;
END |
DELIMITER ;

CALL avant_apres_2010_case(59, @message);   
SELECT @message;
CALL avant_apres_2010_case(62, @message);   
SELECT @message;
CALL avant_apres_2010_case(69, @message);
SELECT @message;

Comportement particulier : aucune correspondance trouvée

En l'absence de clause ELSE, si aucune des conditions posées par les différentes clauses WHEN n'est remplie (quelle que soit la syntaxe utilisée), une erreur est déclenchée.

Par exemple, cette procédure affiche une salutation différente selon la terminaison du nom de l'animal passé en paramètre :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
DELIMITER |
CREATE PROCEDURE salut_nom(IN p_animal_id INT)
BEGIN
    DECLARE v_terminaison CHAR(1);

    SELECT SUBSTRING(nom, -1, 1) INTO v_terminaison  -- Une position négative signifie qu'on recule au lieu d'avancer. 
    FROM Animal                                      --  -1 est donc la dernière lettre du nom.
    WHERE id = p_animal_id;

    CASE v_terminaison
        WHEN 'a' THEN
            SELECT 'Bonjour !' AS Salutations;
        WHEN 'o' THEN
            SELECT 'Salut !' AS Salutations;
        WHEN 'i' THEN
            SELECT 'Coucou !' AS Salutations;
    END CASE;

END|
DELIMITER ;

CALL salut_nom(69);  -- Baba
CALL salut_nom(5);   -- Choupi
CALL salut_nom(29);  -- Fiero
CALL salut_nom(54);  -- Bubulle

Salutations

Bonjour !

Salutations

Coucou !

Salutations

Salut !

1
ERROR 1339 (20000): Case not found for CASE statement

L'appel de la procédure avec Bubulle présente un cas qui n'est pas couvert par les trois WHEN. Une erreur est donc déclenchée

Donc, si l'on n'est pas sûr d'avoir couvert tous les cas possibles, il faut toujours ajouter une clause ELSE pour éviter les erreurs. Si l'on veut qu'aucune instruction ne soit exécutée par le ELSE, il suffit simplement de mettre un bloc d'instructions vide (BEGIN END;).

Exemple : reprenons la procédure salut_nom(), et ajoutons-lui une clause ELSE vide :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
DROP PROCEDURE salut_nom;
DELIMITER |
CREATE PROCEDURE salut_nom(IN p_animal_id INT)
BEGIN
    DECLARE v_terminaison CHAR(1);

    SELECT SUBSTRING(nom, -1, 1) INTO v_terminaison
    FROM Animal
    WHERE id = p_animal_id;

    CASE v_terminaison
        WHEN 'a' THEN
            SELECT 'Bonjour !' AS Salutations;
        WHEN 'o' THEN
            SELECT 'Salut !' AS Salutations;
        WHEN 'i' THEN
            SELECT 'Coucou !' AS Salutations;
        ELSE
            BEGIN                              -- Bloc d'instructions vide
            END;
    END CASE;

END|
DELIMITER ;

CALL salut_nom(69);  -- Baba
CALL salut_nom(5);   -- Choupi
CALL salut_nom(29);  -- Fiero
CALL salut_nom(54);  -- Bubulle

Cette fois, pas d'erreur. Le dernier appel (avec Bubulle) n'affiche simplement rien.

Il faut au minimum une instruction ou un bloc d'instructions par clause WHEN et par clause ELSE. Un bloc vide BEGIN END; est donc nécessaire si l'on ne veut rien exécuter.

Utiliser une structure conditionnelle directement dans une requête

Jusqu'ici, on a vu l'usage des structures conditionnelles dans des procédures stockées. Il est cependant possible d'utiliser une structure CASE dans une simple requête.

Par exemple, écrivons une requête SELECT suivant le même principe que la procédure message_sexe() :

1
2
3
4
5
6
7
SELECT id, nom, CASE
          WHEN sexe = 'M' THEN 'Je suis un mâle !'
          WHEN sexe = 'F' THEN 'Je suis une femelle !'
          ELSE 'Je suis en plein questionnement existentiel...'
       END AS message
FROM Animal
WHERE id IN (9, 8, 6);

id

nom

message

6

Bobosse

Je suis une femelle !

8

Bagherra

Je suis un mâle !

9

NULL

Je suis en plein questionnement existentiel…

Quelques remarques :

  • On peut utiliser les deux syntaxes de CASE.
  • Il faut clôturer le CASE par END, et non par END CASE (et bien sûr ne pas mettre de ; si la requête n'est pas finie).
  • Ce n'est pas limité aux clauses SELECT, on peut tout à fait utiliser un CASE dans une clause WHERE par exemple.
  • Ce n'est par conséquent pas non plus limité aux requêtes SELECT, on peut l'utiliser dans n'importe quelle requête.

Il n'est par contre pas possible d'utiliser une structure IF dans une requête. Cependant, il existe une fonction IF(), beaucoup plus limitée, dont la syntaxe est la suivante :

1
IF(condition, valeur_si_vrai, valeur_si_faux)

Exemple :

1
2
3
SELECT nom, IF(sexe = 'M', 'Je suis un mâle', 'Je ne suis pas un mâle') AS sexe
FROM Animal
WHERE espece_id = 5;

nom

sexe

Baba

Je ne suis pas un mâle

Bibo

Je suis un mâle

Momy

Je ne suis pas un mâle

Popi

Je suis un mâle

Mimi

Je ne suis pas un mâle

Boucles

Une boucle est une structure qui permet de répéter plusieurs fois une série d'instructions. Il existe trois types de boucles en MySQL : WHILE, LOOP et REPEAT.

La boucle WHILE

La boucle WHILE permet de répéter une série d'instructions tant que la condition donnée reste vraie.

1
2
3
WHILE condition DO    -- Attention de ne pas oublier le DO, erreur classique
    instructions
END WHILE;

Exemple : la procédure suivante affiche les nombres entiers de 1 à p_nombre (passé en paramètre).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DELIMITER |
CREATE PROCEDURE compter_jusque_while(IN p_nombre INT)
BEGIN
    DECLARE v_i INT DEFAULT 1;

    WHILE v_i <= p_nombre DO
        SELECT v_i AS nombre; 

        SET v_i = v_i + 1;    -- À ne surtout pas oublier, sinon la condition restera vraie
    END WHILE;
END |
DELIMITER ;

CALL compter_jusque_while(3);

Vérifiez que votre condition devient bien fausse après un certain nombre d'itérations de la boucle. Sinon, vous vous retrouvez avec une boucle infinie (qui ne s'arrête jamais).

La boucle REPEAT

La boucle REPEAT travaille en quelque sorte de manière opposée à WHILE, puisqu'elle exécute des instructions de la boucle jusqu'à ce que la condition donnée devienne vraie.

Exemple : voici la même procédure écrite avec une boucle REPEAT.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
DELIMITER |
CREATE PROCEDURE compter_jusque_repeat(IN p_nombre INT)
BEGIN
    DECLARE v_i INT DEFAULT 1;

    REPEAT
        SELECT v_i AS nombre; 

        SET v_i = v_i + 1;    -- À ne surtout pas oublier, sinon la condition restera vraie
    UNTIL v_i > p_nombre END REPEAT;
END |
DELIMITER ;

CALL compter_jusque_repeat(3);

Attention, comme la condition d'une boucle REPEAT est vérifiée après le bloc d'instructions de la boucle, on passe au moins une fois dans la boucle, même si la condition est tout de suite fausse !

Test

1
2
3
4
5
-- Condition fausse dès le départ, on ne rentre pas dans la boucle
CALL compter_jusque_while(0);   

-- Condition fausse dès le départ, on rentre quand même une fois dans la boucle
CALL compter_jusque_repeat(0);

Donner un label à une boucle

Il est possible de donner un label (un nom) à une boucle, ou à un bloc d'instructions défini par BEGIN... END. Il suffit pour cela de faire précéder l'ouverture de la boucle/du bloc par ce label, suivi de :.

La fermeture de la boucle/du bloc peut alors faire référence à ce label (mais ce n'est pas obligatoire).

Un label ne peut pas dépasser 16 caractères.

Exemples

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- Boucle WHILE
-- ------------
super_while: WHILE condition DO    -- La boucle a pour label "super_while"
    instructions
END WHILE super_while;             -- On ferme en donnant le label de la boucle (facultatif)

-- Boucle REPEAT
-- -------------                  
repeat_genial: REPEAT              -- La boucle s'appelle "repeat_genial"
    instructions
UNTIL condition END REPEAT;        -- Cette fois, on choisit de ne pas faire référence au label lors de la fermeture

-- Bloc d'instructions
-- -------------------
bloc_extra: BEGIN                  -- Le bloc a pour label "bloc_extra"
    instructions
END bloc_extra;

Mais en quoi cela peut-il être utile ?

D'une part, cela peut permettre de clarifier le code lorsqu'il y a beaucoup de boucles et de blocs d'instructions imbriqués. D'autre part, il est nécessaire de donner un label aux boucles et aux blocs d'instructions pour lesquels on veut pouvoir utiliser les instructions ITERATE et LEAVE.

Les instructions LEAVE et ITERATE

LEAVE : quitter la boucle ou le bloc d'instructions

L'instruction LEAVE peut s'utiliser dans une boucle ou un bloc d'instructions et déclenche la sortie immédiate de la structure dont le label est donné.

1
LEAVE label_structure;

Exemple : cette procédure incrémente de 1, et affiche, un nombre entier passé en paramètre. Et cela, 4 fois maximum. Mais si l'on trouve un multiple de 10, la boucle s'arrête.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DELIMITER |
CREATE PROCEDURE test_leave1(IN p_nombre INT)
BEGIN
    DECLARE v_i INT DEFAULT 4;

    SELECT 'Avant la boucle WHILE';

    while1: WHILE v_i > 0 DO

        SET p_nombre = p_nombre + 1;        -- On incrémente le nombre de 1

        IF p_nombre%10 = 0 THEN              -- Si p_nombre est divisible par 10,
            SELECT 'Stop !' AS 'Multiple de 10';
            LEAVE while1;                   -- On quitte la boucle WHILE.
        END IF;

        SELECT p_nombre;                    -- On affiche p_nombre
        SET v_i = v_i - 1;                  -- Attention de ne pas l'oublier

    END WHILE while1;

    SELECT 'Après la boucle WHILE';
END|
DELIMITER ;

CALL test_leave1(3); -- La boucle s'exécutera 4 fois

Avant la boucle WHILE

Avant la boucle WHILE

p_nombre

4

p_nombre

5

p_nombre

6

p_nombre

7

Après la boucle WHILE

Après la boucle WHILE

1
CALL test_leave1(8); -- La boucle s'arrêtera dès qu'on atteint 10

Avant la boucle WHILE

Avant la boucle WHILE

p_nombre

9

Multiple de 10

Stop !

Après la boucle WHILE

Après la boucle WHILE

Il est par conséquent possible d'utiliser LEAVE pour provoquer la fin de la procédure stockée.

Exemple : voici la même procédure. Cette fois-ci un multiple de 10 provoque l'arrêt de toute la procédure, pas seulement de la boucle WHILE.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
DELIMITER |
CREATE PROCEDURE test_leave2(IN p_nombre INT)
corps_procedure: BEGIN                           -- On donne un label au bloc d'instructions principal
    DECLARE v_i INT DEFAULT 4;

    SELECT 'Avant la boucle WHILE';
    while1: WHILE v_i > 0 DO
        SET p_nombre = p_nombre + 1;             -- On incrémente le nombre de 1
        IF p_nombre%10 = 0 THEN                  -- Si p_nombre est divisible par 10,
            SELECT 'Stop !' AS 'Multiple de 10';
            LEAVE corps_procedure;               -- je quitte la procédure.
        END IF;

        SELECT p_nombre;                         -- On affiche p_nombre
        SET v_i = v_i - 1;                       -- Attention de ne pas l'oublier
    END WHILE while1;

    SELECT 'Après la boucle WHILE';
END|
DELIMITER ;

CALL test_leave2(8);

'Après la boucle WHILE' ne s'affiche plus lorsque l'instruction LEAVE est déclenchée, puisque l'on quitte la procédure stockée avant d'arriver à l'instruction SELECT qui suit la boucle WHILE.

En revanche, LEAVE ne permet pas de quitter directement une structure conditionnelle (IF ou CASE). Il n'est d'ailleurs pas non plus possible de donner un label à ces structures. Cette restriction est cependant aisément contournable en utilisant les blocs d'instructions.

Exemple : la procédure suivante affiche les nombres de 4 à 1, en précisant s'ils sont pairs. Sauf pour le nombre 2, pour lequel une instruction LEAVE empêche l'affichage habituel.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
DELIMITER |
CREATE PROCEDURE test_leave3()
BEGIN
    DECLARE v_i INT DEFAULT 4;

    WHILE v_i > 0 DO

        IF v_i%2 = 0 THEN
            if_pair: BEGIN
                IF v_i = 2 THEN                   -- Si v_i vaut 2
                    LEAVE if_pair;                -- On quitte le bloc "if_pair", ce qui revient à quitter la structure IF v_i%2 = 0
                END IF;
                SELECT CONCAT(v_i, ' est pair') AS message;
            END if_pair; 
        ELSE    
            if_impair: BEGIN
                SELECT CONCAT(v_i, ' est impair') AS message;
            END if_impair;   
        END IF;

        SET v_i = v_i - 1;
    END WHILE;
END|
DELIMITER ;

CALL test_leave3();

message

4 est pair

message

3 est impair

message

1 est impair

'2 est pair' n'est pas affiché, puisqu'on a quitté le IF avant cet affichage.

ITERATE : déclencher une nouvelle itération de la boucle

Cette instruction ne peut être utilisée que dans une boucle. Lorsqu'elle est exécutée, une nouvelle itération de la boucle commence. Toutes les instructions suivant ITERATE dans la boucle sont ignorées.

Exemple : la procédure suivante affiche les nombres de 1 à 3, avec un message avant le IF et après le IF. Sauf pour le nombre 2, qui relance une itération de la boucle dans le IF.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
DELIMITER |
CREATE PROCEDURE test_iterate()
BEGIN
    DECLARE v_i INT DEFAULT 0;

    boucle_while: WHILE v_i < 3 DO
        SET v_i = v_i + 1;
        SELECT v_i, 'Avant IF' AS message;

        IF v_i = 2 THEN
            ITERATE boucle_while;
        END IF;

        SELECT v_i, 'Après IF' AS message;  -- Ne sera pas exécuté pour v_i = 2
    END WHILE;
END |
DELIMITER ;

CALL test_iterate();

v_i

message

1

Avant IF

v_i

message

1

Après IF

v_i

message

2

Avant IF

v_i

message

3

Avant IF

v_i

message

3

Après IF

Attention à ne pas faire de boucle infinie avec ITERATE, on oublie facilement que cette instruction empêche l'exécution de toutes les instructions qui la suivent dans la boucle. Si j'avais mis par exemple SET v_i = v_i + 1; après ITERATE et non avant, la boucle serait restée coincée à v_i = 2.

La boucle LOOP

On a gardé la boucle LOOP pour la fin, parce qu'elle est un peu particulière. En effet, voici sa syntaxe :

1
2
3
[label:] LOOP
    instructions
END LOOP [label]

Vous voyez bien : il n'est question de condition nulle part. En fait, une boucle LOOP doit intégrer dans ses instructions un élément qui va la faire s'arrêter : typiquement une instruction LEAVE. Sinon, c'est une boucle infinie.

Exemple : à nouveau une procédure qui affiche les nombres entiers de 1 à p_nombre.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
DELIMITER |
CREATE PROCEDURE compter_jusque_loop(IN p_nombre INT)
BEGIN
    DECLARE v_i INT DEFAULT 1;

    boucle_loop: LOOP
        SELECT v_i AS nombre; 

        SET v_i = v_i + 1;

        IF v_i > p_nombre THEN
            LEAVE boucle_loop;
        END IF;    
    END LOOP;
END |
DELIMITER ;

CALL compter_jusque_loop(3);

En résumé

  • Un bloc d'instructions est délimité par BEGIN et END. Il est possible d'imbriquer plusieurs blocs d'instructions.
  • Une variable locale est définie dans un bloc d'instructions grâce à la commande DECLARE. Une fois la fin du bloc d'instructions atteinte, toutes les variables locales qui y ont été déclarées sont supprimées.
  • Une structure conditionnelle permet d'exécuter une série d'instructions si une condition est respectée. Les deux structures conditionnelles de MySQL sont IF et CASE.
  • Une boucle est une structure qui permet de répéter une série d'instructions un certain nombre de fois. Il existe trois types de boucle pour MySQL : WHILE, REPEAT et LOOP.
  • L'instruction LEAVE permet de quitter un bloc d'instructions ou une boucle.
  • L'instruction ITERATE permet de relancer une itération d'une boucle.