Chapitre 620 minAvancé
Gérer les erreurs : DECLARE HANDLER, SIGNAL et GET DIAGNOSTICS
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- intercepter une erreur précise ou toute erreur avec
DECLARE … HANDLER; - choisir entre un gestionnaire
CONTINUEetEXIT; - lever ta propre erreur avec
SIGNALet la relayer avecRESIGNAL; - lire le détail d'une erreur avec
GET DIAGNOSTICSet le journaliser.
4 exercices corrigés1 schéma
Gérer les erreurs dans une procédure stockée MySQL, c'est décider ce qui se passe quand une instruction échoue : un gestionnaire déclaré avec DECLARE … HANDLER intercepte l'erreur, SIGNAL lève une erreur métier avec ton propre message, RESIGNAL la renvoie à l'appelant, et GET DIAGNOSTICS en lit le détail. Ce chapitre montre chaque mécanisme sur la base parc, et comment journaliser les erreurs dans une table.
Ce qui se passe sans gestionnaire
Cette procédure clôture un ticket en deux temps, sans aucune gestion d'erreur :
DELIMITER //
CREATE PROCEDURE cloturer_naif(IN p_id_ticket INT, IN p_id_technicien INT, IN p_duree_min SMALLINT)
BEGIN
UPDATE ticket SET statut = 'resolu', date_cloture = NOW() WHERE id_ticket = p_id_ticket;
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min)
VALUES (p_id_ticket, p_id_technicien, NOW(), p_duree_min);
END //
DELIMITER ;
CALL cloturer_naif(14, 11, 0);
-- ERROR 3819 (HY000): Check constraint 'ck_intervention_duree' is violated.
SELECT statut FROM ticket WHERE id_ticket = 14; -- resolu
L'INSERT échoue (une durée de 0 minute viole la contrainte CHECK), la procédure s'arrête et l'erreur remonte à l'appelant. Mais l'UPDATE, lui, a réussi et reste en place : le ticket 14 est résolu sans aucune intervention. Une erreur arrête la procédure, elle n'annule pas ce qui précède. C'est le rôle des transactions (chapitre 7) ; ce chapitre donne d'abord les outils pour réagir à l'erreur.
Pour remettre le ticket dans son état : UPDATE ticket SET statut = 'ouvert', date_cloture = NULL WHERE id_ticket = 14;.
Codes d'erreur et SQLSTATE
Chaque erreur MySQL porte deux codes : un numéro MySQL (1062) et un SQLSTATE à cinq caractères, normalisé (23000). Plusieurs numéros partagent souvent le même SQLSTATE.
| Numéro | SQLSTATE | Message (début) | Cause typique dans parc |
|---|---|---|---|
| 1062 | 23000 | Duplicate entry '1-3' for key … | Installer un logiciel déjà installé sur le poste |
| 1452 | 23000 | Cannot add or update a child row: a foreign key constraint fails | Installer sur un poste qui n'existe pas |
| 1451 | 23000 | Cannot delete or update a parent row: a foreign key constraint fails | Supprimer un logiciel encore installé |
| 1048 | 23000 | Column '…' cannot be null | Oublier une valeur obligatoire |
| 3819 | HY000 | Check constraint '…' is violated | Une durée d'intervention de 0 minute |
| 1406 | 22001 | Data too long for column '…' | Un titre de plus de 100 caractères |
| 1644 | 45000 | Ton message | Une erreur levée par SIGNAL |
| 1329 | 02000 | No data - zero rows fetched, selected, or processed | Un FETCH après la dernière ligne |
| 1213 | 40001 | Deadlock found when trying to get lock | Deux transactions qui s'attendent (interblocage) |
| 1205 | HY000 | Lock wait timeout exceeded | Une ligne verrouillée trop longtemps |
Les deux premiers caractères du SQLSTATE donnent la classe : 00 succès, 01 avertissement, 02 « pas de donnée », tout le reste est une erreur. MySQL regroupe ces classes sous trois noms que tu utiliseras dans les gestionnaires : SQLWARNING (classe 01), NOT FOUND (classe 02) et SQLEXCEPTION (toutes les autres).
DECLARE … HANDLER
Un gestionnaire dit : « si cette condition survient dans ce bloc, exécute ceci ».
DECLARE { CONTINUE | EXIT } HANDLER FOR condition [, condition …]
instruction; -- ou un bloc BEGIN … END
La condition est un numéro d'erreur MySQL (1062), un SQLSTATE (SQLSTATE '23000'), l'une des trois classes (SQLWARNING, NOT FOUND, SQLEXCEPTION) ou une condition nommée (plus bas). Après l'exécution du gestionnaire :
CONTINUE: la procédure reprend à l'instruction qui suit celle qui a échoué ;EXIT: le blocBEGIN … ENDqui déclare le gestionnaire se termine. Si c'est le bloc principal, la procédure se termine, sans erreur pour l'appelant (sauf si le gestionnaire fait unRESIGNAL).
Une procédure d'installation qui transforme les erreurs prévisibles en message clair, au lieu de les laisser remonter :
DELIMITER //
CREATE PROCEDURE ajouter_installation(
IN p_id_poste INT,
IN p_id_logiciel INT,
OUT p_message VARCHAR(100)
)
BEGIN
DECLARE CONTINUE HANDLER FOR 1062 SET p_message = 'Ce logiciel est déjà installé sur ce poste';
DECLARE CONTINUE HANDLER FOR 1452 SET p_message = 'Poste ou logiciel inconnu';
SET p_message = 'Installation enregistrée';
INSERT INTO installation (id_poste, id_logiciel, date_installation)
VALUES (p_id_poste, p_id_logiciel, CURDATE());
END //
DELIMITER ;
CALL ajouter_installation(14, 3, @m); SELECT @m; -- Installation enregistrée
CALL ajouter_installation(1, 3, @m); SELECT @m; -- Ce logiciel est déjà installé sur ce poste
CALL ajouter_installation(99, 3, @m); SELECT @m; -- Poste ou logiciel inconnu
Le message par défaut est écrit avant l'INSERT ; si l'INSERT échoue, le gestionnaire le remplace, puis la procédure continue jusqu'à son END. Pour annuler l'installation de test : DELETE FROM installation WHERE id_poste = 14 AND id_logiciel = 3;.
Quel gestionnaire s'exécute ?
Quand plusieurs gestionnaires correspondent à une erreur, MySQL choisit le plus précis : un numéro d'erreur MySQL l'emporte sur un SQLSTATE, qui l'emporte sur une classe (SQLEXCEPTION). Dans cette procédure, chaque INSERT échoue et déclenche un gestionnaire différent :
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION SELECT 'SQLEXCEPTION' AS gestionnaire;
DECLARE CONTINUE HANDLER FOR SQLSTATE '23000' SELECT 'SQLSTATE 23000' AS gestionnaire;
DECLARE CONTINUE HANDLER FOR 1062 SELECT '1062' AS gestionnaire;
INSERT INTO installation VALUES (1, 3, CURDATE()); -- doublon : 1062
INSERT INTO installation VALUES (99, 3, CURDATE()); -- poste inconnu : 1452, SQLSTATE 23000
INSERT INTO intervention (id_ticket, id_technicien, date_intervention, duree_min)
VALUES (1, 11, NOW(), 0); -- contrainte CHECK : 3819, SQLSTATE HY000
Le premier affiche 1062, le deuxième SQLSTATE 23000, le troisième SQLEXCEPTION. Sur MariaDB, le troisième affiche SQLSTATE 23000, puisque la violation d'une contrainte CHECK y porte ce SQLSTATE (encadré ci-dessus).
Deux règles de portée complètent le tableau :
- un gestionnaire ne s'applique qu'aux erreurs de son bloc (et des blocs imbriqués). Un gestionnaire déclaré dans un sous-bloc
BEGIN … ENDne rattrape pas une erreur survenue après ce sous-bloc ; - une erreur non traitée dans une procédure appelée remonte à la procédure appelante, dont les gestionnaires peuvent la rattraper. La procédure appelée s'arrête à l'erreur ; l'appelante, elle, continue si son gestionnaire est
CONTINUE.
Nommer une condition
DECLARE … CONDITION donne un nom à un code, ce qui rend les gestionnaires lisibles :
DECLARE doublon CONDITION FOR 1062;
DECLARE poste_inconnu CONDITION FOR SQLSTATE '45000';
DECLARE CONTINUE HANDLER FOR doublon SET p_message = 'Déjà installé';
Une condition se déclare avec les variables, avant les curseurs et les gestionnaires.
SIGNAL : lever ta propre erreur
SIGNAL lève une erreur, exactement comme si une instruction avait échoué. C'est ainsi qu'une procédure ou un trigger refuse une opération qui viole une règle de gestion :
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Ticket inconnu';
SQLSTATE '45000'est le code réservé aux erreurs définies par l'utilisateur (« exception non gérée définie par l'utilisateur ») ; c'est celui qu'on utilise presque toujours ;MESSAGE_TEXTest le message que reçoit l'appelant. Sans lui, le message estUnhandled user-defined exception condition, ce qui n'aide personne ;- côté client, l'erreur arrive avec le numéro 1644 :
ERROR 1644 (45000): Ticket inconnu. C'est ce numéro que l'application teste pour reconnaître une erreur métier (chapitre 12) ; MYSQL_ERRNO = 5001remplace ce numéro par le tien, pour distinguer plusieurs erreurs métier côté application :ERROR 5001 (45000): ….
Le message accepte une variable, ce qui permet un message précis :
DECLARE v_message VARCHAR(128);
…
SET v_message = CONCAT('Plus de licence ', v_nom, ' : ', v_installees, ' installations pour ',
v_licences, ' licences');
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message;
Un SIGNAL avec un SQLSTATE de classe 01 lève un simple avertissement : la procédure continue, et l'avertissement apparaît dans SHOW WARNINGS. SIGNAL fonctionne aussi hors d'une routine, directement dans le client, ce qui est pratique pour tester le message que verra l'application.
RESIGNAL : relayer une erreur
Dans un gestionnaire, RESIGNAL relance l'erreur qui l'a déclenché, telle quelle ou avec un message modifié. Le gestionnaire peut ainsi faire quelque chose (annuler, journaliser) et prévenir l'appelant :
DELIMITER //
CREATE PROCEDURE supprimer_logiciel(IN p_id_logiciel INT)
BEGIN
DECLARE EXIT HANDLER FOR 1451
RESIGNAL SET MESSAGE_TEXT = 'Logiciel encore installé : désinstalle-le d''abord';
DELETE FROM logiciel WHERE id_logiciel = p_id_logiciel;
END //
DELIMITER ;
CALL supprimer_logiciel(5);
-- ERROR 1451 (23000): Logiciel encore installé : désinstalle-le d'abord
Le numéro et le SQLSTATE d'origine sont conservés, seul le message change : le message technique de la clé étrangère, qui révèle les noms des tables et des contraintes, ne sort pas de la base. RESIGNAL n'est permis que dans un gestionnaire.
GET DIAGNOSTICS : lire le détail d'une erreur
Un gestionnaire SQLEXCEPTION rattrape toutes les erreurs : pour savoir laquelle, on interroge la zone de diagnostic avec GET DIAGNOSTICS.
GET DIAGNOSTICS CONDITION 1
v_etat = RETURNED_SQLSTATE, -- '23000'
v_num = MYSQL_ERRNO, -- 1062
v_msg = MESSAGE_TEXT; -- 'Duplicate entry …'
GET DIAGNOSTICS v_nb = NUMBER, v_lignes = ROW_COUNT; lit les informations sur l'instruction elle-même : le nombre de conditions et le nombre de lignes modifiées par la dernière instruction.
La zone de diagnostic est remplacée par chaque nouvelle instruction. Dans un gestionnaire, lis-la en premier, avant toute autre instruction ; ou utilise GET STACKED DIAGNOSTICS, qui lit la zone telle qu'elle était au déclenchement du gestionnaire, même après d'autres instructions. GET DIAGNOSTICS seul équivaut à GET CURRENT DIAGNOSTICS.
Journaliser les erreurs
Le modèle complet : un gestionnaire EXIT qui lit le diagnostic, l'écrit dans la table journal_erreur, puis relance l'erreur pour que l'appelant sache que l'opération a échoué.
DELIMITER //
CREATE PROCEDURE changer_priorite(IN p_id_ticket INT, IN p_priorite VARCHAR(10))
BEGIN
DECLARE v_etat CHAR(5);
DECLARE v_num INT;
DECLARE v_msg VARCHAR(512);
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1
v_etat = RETURNED_SQLSTATE, v_num = MYSQL_ERRNO, v_msg = MESSAGE_TEXT;
INSERT INTO journal_erreur (routine, etat_sql, num_erreur, message)
VALUES ('changer_priorite', v_etat, v_num, v_msg);
RESIGNAL;
END;
IF NOT EXISTS (SELECT 1 FROM ticket WHERE id_ticket = p_id_ticket) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Ticket inconnu';
END IF;
UPDATE ticket SET priorite = p_priorite WHERE id_ticket = p_id_ticket;
END //
DELIMITER ;
CALL changer_priorite(12, 'urgente');
-- ERROR 3819 (HY000): Check constraint 'ck_ticket_priorite' is violated.
CALL changer_priorite(99, 'haute');
-- ERROR 1644 (45000): Ticket inconnu
SELECT id_erreur, routine, etat_sql, num_erreur, message FROM journal_erreur;
| id_erreur | routine | etat_sql | num_erreur | message |
|---|---|---|---|---|
| 1 | changer_priorite | HY000 | 3819 | Check constraint 'ck_ticket_priorite' is violated. |
| 2 | changer_priorite | 45000 | 1644 | Ticket inconnu |
Trois remarques :
- le
SIGNALde la procédure est rattrapé par son propre gestionnaireSQLEXCEPTION(la classe45est une erreur) : il est journalisé comme les autres ; RESIGNALrelance l'erreur d'origine, même après l'INSERTdu gestionnaire : l'appelant reçoit bien l'erreur 3819 ou 1644 ;- la ligne du journal reste, bien que l'opération ait échoué, parce qu'en mode autocommit chaque instruction est validée à part. Dans une procédure transactionnelle, l'ordre compte :
ROLLBACKd'abord, journal ensuite, sinon leROLLBACKeffacerait la trace (chapitre 7).
Exercices
Exercice 1 sur 4 CONTINUE ou EXIT Réussi À revoir
Qu'affiche l'appel CALL test_gestionnaire(); ? Et si l'on remplace CONTINUE par EXIT ?
DELIMITER //
CREATE PROCEDURE test_gestionnaire()
BEGIN
DECLARE v_etape VARCHAR(30) DEFAULT 'début';
DECLARE CONTINUE HANDLER FOR 1062 SET v_etape = CONCAT(v_etape, ', doublon');
SET v_etape = CONCAT(v_etape, ', avant');
INSERT INTO installation VALUES (1, 3, CURDATE()); -- Firefox est déjà sur le poste 1
SET v_etape = CONCAT(v_etape, ', après');
SELECT v_etape;
END //
DELIMITER ;
Voir la réponseMasquer la réponse de l'exercice 1
Avec CONTINUE : début, avant, doublon, après. Le gestionnaire s'exécute, puis la procédure reprend à l'instruction qui suit l'INSERT.
Avec EXIT : rien ne s'affiche. Le gestionnaire s'exécute (v_etape devient début, avant, doublon), puis le bloc principal se termine : le SELECT final n'est jamais atteint. L'appelant ne reçoit aucune erreur non plus : vu de l'extérieur, la procédure a réussi sans rien faire. C'est pourquoi un gestionnaire EXIT se termine presque toujours par un RESIGNAL ou par l'affectation d'un paramètre OUT qui dit ce qui s'est passé.
Tu avais trouvé ?
Exercice 2 sur 4 Un contrôle avec SIGNAL Réussi À revoir
Écris une procédure affecter_poste(IN p_id_poste INT, IN p_id_employe INT) qui affecte un poste à un employé. Elle refuse, avec un message clair : un poste inconnu, un employé inconnu, un poste déjà affecté à quelqu'un d'autre. Donne le résultat de CALL affecter_poste(4, 14); et de CALL affecter_poste(14, 14);.
Voir la réponseMasquer la réponse de l'exercice 2
DELIMITER //
CREATE PROCEDURE affecter_poste(IN p_id_poste INT, IN p_id_employe INT)
BEGIN
DECLARE v_existe INT;
DECLARE v_actuel INT;
DECLARE v_message VARCHAR(128);
SELECT COUNT(*), MAX(id_employe) INTO v_existe, v_actuel
FROM poste WHERE id_poste = p_id_poste;
IF v_existe = 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Poste inconnu';
END IF;
IF NOT EXISTS (SELECT 1 FROM employe WHERE id_employe = p_id_employe) THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Employé inconnu';
END IF;
IF v_actuel IS NOT NULL AND v_actuel <> p_id_employe THEN
SET v_message = CONCAT('Poste déjà affecté à l''employé ', v_actuel);
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_message;
END IF;
UPDATE poste SET id_employe = p_id_employe WHERE id_poste = p_id_poste;
END //
DELIMITER ;
CALL affecter_poste(4, 14); répond ERROR 1644 (45000): Poste déjà affecté à l'employé 4 : le PC-0004 est à Sofiane Benali. CALL affecter_poste(14, 14); affecte le PC-0014, en stock, à Paul Martin.
COUNT(*) vaut 0 ou 1 et distingue « poste inconnu » de « poste en stock » : avec un simple SELECT id_employe INTO …, les deux cas donneraient NULL. MAX() est là parce qu'avec COUNT(*) la requête est un agrégat : chaque colonne du SELECT doit être agrégée. Un SIGNAL arrête la procédure : les contrôles suivants ne s'exécutent pas.
Tu avais trouvé ?
Exercice 3 sur 4 Traduire les erreurs techniques Réussi À revoir
La procédure ajouter_installation utilise deux gestionnaires CONTINUE. Réécris-la pour qu'elle lève une erreur avec un message clair, au lieu de renvoyer un message dans un paramètre OUT, en gardant les numéros d'erreur d'origine. Pourquoi est-ce préférable pour une application ?
Voir la réponseMasquer la réponse de l'exercice 3
DELIMITER //
CREATE PROCEDURE ajouter_installation(IN p_id_poste INT, IN p_id_logiciel INT)
BEGIN
DECLARE EXIT HANDLER FOR 1062
RESIGNAL SET MESSAGE_TEXT = 'Ce logiciel est déjà installé sur ce poste';
DECLARE EXIT HANDLER FOR 1452
RESIGNAL SET MESSAGE_TEXT = 'Poste ou logiciel inconnu';
INSERT INTO installation (id_poste, id_logiciel, date_installation)
VALUES (p_id_poste, p_id_logiciel, CURDATE());
END //
DELIMITER ;
Une erreur ne peut pas être ignorée par distraction : avec un paramètre OUT, un développeur qui oublie de lire le message croit l'installation réussie. L'erreur arrive dans l'application comme une exception, avec le numéro 1062 ou 1452 et un message lisible, et le message technique de MySQL ne sort pas de la base. Pense à supprimer l'ancienne version avant de recréer la procédure (DROP PROCEDURE IF EXISTS ajouter_installation;).
Tu avais trouvé ?
Exercice 4 sur 4 Lire un journal d'erreurs Réussi À revoir
Le responsable trouve ces lignes dans journal_erreur. Pour chacune, dis ce que l'utilisateur a probablement tenté et comment l'éviter côté application.
| routine | etat_sql | num_erreur | message |
|---|---|---|---|
| changer_priorite | HY000 | 3819 | Check constraint 'ck_ticket_priorite' is violated. |
| cloturer_ticket | 23000 | 1452 | Cannot add or update a child row: a foreign key constraint fails (…fk_intervention_technicien…) |
| installer_logiciel | 40001 | 1213 | Deadlock found when trying to get lock; try restarting transaction |
Voir la réponseMasquer la réponse de l'exercice 4
- Une priorité hors de la liste autorisée (une faute de frappe, ou une valeur comme
urgente). L'application doit proposer une liste déroulante des quatre valeurs, et la procédure peut vérifier la valeur elle-même pour renvoyer un message clair. - Une clôture avec un identifiant de technicien qui n'existe pas dans
employe. L'application doit prendre l'identifiant du technicien connecté, jamais une valeur saisie librement. - Deux installations simultanées qui se sont bloquées mutuellement : InnoDB a annulé l'une des deux. Ce n'est pas une faute de l'utilisateur : l'application (ou la procédure) doit rejouer la transaction (chapitre 7).
Tu avais trouvé ?