- Gestionnaires d'erreurs, curseurs et utilisation avancée
- Au-delà des tables classiques : vues, tables temporaires et vues matérialisées
Les triggers (ou déclencheurs) sont des objets de la base de données. Attachés à une table, ils vont déclencher l'exécution d'une instruction, ou d'un bloc d'instructions, lorsqu'une, ou plusieurs lignes sont insérées, supprimées ou modifiées dans la table à laquelle ils sont attachés.
Dans ce chapitre, nous allons voir comment ils fonctionnent exactement, comment on peut les créer et les supprimer, et surtout, comment on peut s'en servir et quelles sont leurs restrictions.
Principe et usage
Qu'est-ce qu'un trigger ?
Tout comme les procédures stockées, les triggers servent à exécuter une ou plusieurs instructions. Mais à la différence des procédures, il n'est pas possible d'appeler un trigger : un trigger doit être déclenché par un événement.
Un trigger est attaché à une table, et peut être déclenché par :
- une insertion dans la table (requête
INSERT
) ; - la suppression d'une partie des données de la table (requête
DELETE
) ; - la modification d'une partie des données de la table (requête
UPDATE
).
Par ailleurs, une fois le trigger déclenché, ses instructions peuvent être exécutées soit juste avant l'exécution de l'événement déclencheur, soit juste après.
Que fait un trigger ?
Un trigger exécute un traitement pour chaque ligne insérée, modifiée ou supprimée par l'événement déclencheur. Donc si l'on insère cinq lignes, les instructions du trigger seront exécutées cinq fois, chaque itération permettant de traiter les données d'une des lignes insérées.
Les instructions d'un trigger suivent les mêmes principes que les instructions d'une procédure stockée. S'il y a plus d'une instruction, il faut les mettre à l'intérieur d'un bloc d'instructions. Les structures que nous avons vues dans les deux chapitres précédents sont bien sûr utilisables (structures conditionnelles, boucles, gestionnaires d'erreur, etc.), avec toutefois quelques restrictions que nous verrons en fin de chapitre.
Un trigger peut modifier et/ou insérer des données dans n'importe quelle table sauf les tables utilisées dans la requête qui l'a déclenché. En ce qui concerne la table à laquelle le trigger est attaché (qui est forcément utilisée par l'événement déclencheur), le trigger peut lire et modifier uniquement la ligne insérée, modifiée ou supprimée qu'il est en train de traiter.
À quoi sert un trigger ?
On peut faire de nombreuses choses avec un trigger. Voici quelques exemples d'usage fréquent de ces objets. Nous verrons plus loin certains de ces exemples appliqués à notre élevage d'animaux.
Contraintes et vérifications de données
Comme cela a déjà été mentionné dans le chapitre sur les types de données, MySQL n'implémente pas de contraintes d'assertion, qui sont des contraintes permettant de limiter les valeurs acceptées par une colonne (limiter une colonne TINYINT
à TRUE
(1) ou FALSE
(0) par exemple).
Avec des triggers se déclenchant avant l'INSERT
et avant l'UPDATE
, on peut vérifier les valeurs d'une colonne lors de l'insertion ou de la modification, et les corriger si elles ne font pas partie des valeurs acceptables, ou bien faire échouer la requête. On peut ainsi pallier l'absence de contraintes d'assertion.
Intégrité des données
Les triggers sont parfois utilisés pour remplacer les options des clés étrangères ON UPDATE RESTRICT|CASCADE|SET NULL
et ON DELETE RESTRICT|CASCADE|SET NULL
. Notamment pour des tables MyISAM, qui sont non-transactionnelles et ne supportent pas les clés étrangères.
Cela peut aussi être utilisé avec des tables transactionnelles, dans les cas où le traitement à appliquer pour garder des données cohérentes est plus complexe que ce qui est permis par les options de clés étrangères.
Par exemple, dans certains systèmes, on veut pouvoir appliquer deux systèmes de suppression :
- une vraie suppression pure et dure, avec effacement des données, donc une requête
DELETE
; - un archivage, qui masquera les données dans l'application mais les conservera dans la base de données.
Dans ce cas, une solution possible est d'ajouter aux tables contenant des données archivables une colonne archive, pouvant contenir 0 (la ligne n'est pas archivée) ou 1 (la ligne est archivée). Pour une vraie suppression, on peut utiliser simplement un ON DELETE RESTRICT|CASCADE|SET NULL
, qui se répercutera sur les tables référençant les données supprimées. Par contre, dans le cas d'un archivage, on utilisera plutôt un trigger pour traiter les lignes qui référencent les données archivées, par exemple en les archivant également.
Historisation des actions
On veut parfois garder une trace des actions effectuées sur la base de données, c'est-à-dire par exemple, savoir qui a modifié telle ligne, et quand. Avec les triggers, rien de plus simple, il suffit de mettre à jour des données d'historisation à chaque insertion, modification ou suppression. Soit directement dans la table concernée, soit dans une table utilisée spécialement et exclusivement pour garder un historique des actions.
Mise à jour d'informations qui dépendent d'autres données
Comme pour les procédures stockées, une partie de la logique "business" de l'application peut être codée directement dans la base de données, grâce aux triggers, plutôt que du côté applicatif (en PHP, Java ou quel que soit le langage de programmation utilisé). À nouveau, cela peut permettre d'harmoniser un traitement à travers plusieurs applications utilisant la même base de données.
Par ailleurs, lorsque certaines informations dépendent de la valeur de certaines données, on peut en général les retrouver en faisant une requête SELECT
. Dans ce cas, il n'est pas indispensable de stocker ces informations.
Cependant, utiliser les triggers pour stocker ces informations peut faciliter la vie de l'utilisateur, et peut aussi faire gagner en performance. Par exemple, si l'on a très souvent besoin de cette information, ou si la requête à faire pour trouver cette information est longue à exécuter.
C'est typiquement cet usage qui est fait des triggers dans ce qu'on appelle les "vues matérialisées", auxquelles un chapitre est consacré dans la partie 6.
Création des triggers
Syntaxe
Pour créer un trigger, on utilise la commande suivante :
1 2 3 | CREATE TRIGGER nom_trigger moment_trigger evenement_trigger ON nom_table FOR EACH ROW corps_trigger |
CREATE TRIGGER nom_trigger
: les triggers ont donc un nom.moment_trigger evenement_trigger
: servent à définir quand et comment le trigger est déclenché.ON nom_table
: c'est là qu'on définit à quelle table le trigger est attaché.FOR EACH ROW
: signifie littéralement "pour chaque ligne", sous-entendu "pour chaque ligne insérée/supprimée/modifiée" selon ce qui a déclenché le trigger.corps_trigger
: c'est le contenu du trigger. Comme pour les procédures stockées, il peut s'agir soit d'une seule instruction, soit d'un bloc d'instructions.
Événement déclencheur
Trois événements différents peuvent déclencher l'exécution des instructions d'un trigger.
- L'insertion de lignes (
INSERT
) dans la table attachée au trigger. - La modification de lignes (
UPDATE
) de cette table. - La suppression de lignes (
DELETE
) de la table.
Un trigger est soit déclenché par INSERT
, soit par UPDATE
, soit par DELETE
. Il ne peut pas être déclenché par deux événements différents. On peut par contre créer plusieurs triggers par table pour couvrir chaque événement.
Avant ou après
Lorsqu'un trigger est déclenché, ses instructions peuvent être exécutées à deux moments différents. Soit juste avant que l'événement déclencheur n'ait lieu (BEFORE
), soit juste après (AFTER
).
Donc, si vous avez un trigger BEFORE UPDATE
sur la table A, l'exécution d'une requête UPDATE
sur cette table va d'abord déclencher l'exécution des instructions du trigger, ensuite seulement les lignes de la table seront modifiées.
Exemple
Pour créer un trigger sur la table Animal, déclenché par une insertion, et s'exécutant après ladite insertion, on utilisera la syntaxe suivante :
1 2 3 | CREATE TRIGGER after_insert_animal AFTER INSERT ON Animal FOR EACH ROW corps_trigger; |
Règle et convention
Il ne peut exister qu'un seul trigger par combinaison moment_trigger/evenement_trigger
par table. Donc un seul trigger BEFORE UPDATE
par table, un seul AFTER DELETE
, etc.
Étant donné qu'il existe deux possibilités pour le moment d'exécution, et trois pour l'événement déclencheur, on a donc un maximum de six triggers par table.
Cette règle étant établie, il existe une convention quant à la manière de nommer ses triggers, que je vous encourage à suivre : nom_trigger = moment_evenement_table. Donc le trigger BEFORE UPDATE ON Animal
aura pour nom : before_update_animal.
OLD et NEW
Dans le corps du trigger, MySQL met à disposition deux mots-clés : OLD
et NEW
.
OLD
: représente les valeurs des colonnes de la ligne traitée avant qu'elle ne soit modifiée par l'événement déclencheur. Ces valeurs peuvent être lues, mais pas modifiées.NEW
: représente les valeurs des colonnes de la ligne traitée après qu'elle a été modifiée par l'événement déclencheur. Ces valeurs peuvent être lues et modifiées.
Il n'y a que dans le cas d'un trigger UPDATE
que OLD
et NEW
coexistent. Lors d'une insertion, OLD
n'existe pas, puisque la ligne n'existe pas avant l'événement déclencheur ; dans le cas d'une suppression, c'est NEW
qui n'existe pas, puisque la ligne n'existera plus après l'événement déclencheur.
Premier exemple : l'insertion d'une ligne.
Exécutons la commande suivante :
1 2 | INSERT INTO Adoption (client_id, animal_id, date_reservation, prix, paye) VALUES (12, 15, NOW(), 200.00, FALSE); |
Pendant le traitement de cette ligne par le trigger correspondant,
NEW.client_id
vaudra 12 ;NEW.animal_id
vaudra 15 ;NEW.date_reservation
vaudraNOW()
;NEW.date_adoption
vaudraNULL
;NEW.prix
vaudra 200.00 ;NEW.paye
vaudraFALSE
(0).
Les valeurs de OLD
ne seront pas définies.
Dans le cas d'une suppression, on aura exactement l'inverse.
Second exemple : la modification d'une ligne. On modifie la ligne que l'on vient d'insérer en exécutant la commande suivante :
1 2 3 | UPDATE Adoption SET paye = TRUE WHERE client_id = 12 AND animal_id = 15; |
Pendant le traitement de cette ligne par le trigger correspondant,
NEW.paye
vaudraTRUE
, tandis queOLD.paye
vaudraFALSE
.- Par contre les valeurs respectives de
NEW.animal_id, NEW.client_id, NEW.date_reservation
,NEW.date_adoption
etNEW.prix
seront les mêmes queOLD.animal_id, OLD.client_id, OLD.date_reservation
,OLD.date_adoption
etOLD.prix
, puisque ces colonnes ne sont pas modifiées par la requête.
Dans le cas d'une insertion ou d'une modification, si un trigger peut potentiellement changer la valeur de NEW.colonne
, il doit être exécuté avant l'événement (BEFORE
). Sinon, la ligne aura déjà été insérée ou modifiée, et la modification de NEW.colonne
n'aura plus aucune influence sur celle-ci.
Erreur déclenchée pendant un trigger
- Si un trigger
BEFORE
génère une erreur (non interceptée par un gestionnaire d'erreur), la requête ayant déclenché le trigger ne sera pas exécutée. Si l'événement devait également déclencher un triggerAFTER
, il ne sera bien sûr pas non plus exécuté. - Si un trigger
AFTER
génère une erreur, la requête ayant déclenché le trigger échouera. - Dans le cas d'une table transactionnelle, si une erreur est déclenchée, un
ROLLBACK
sera fait. Dans le cas d'une table non-transactionnelle, tous les changements qui auraient été faits par le (ou les) trigger(s) avant le déclenchement de l'erreur persisteront.
Suppression des triggers
Encore une fois, la commande DROP
permet de supprimer un trigger.
1 | DROP TRIGGER nom_trigger; |
Tout comme pour les procédures stockées, il n'est pas possible de modifier un trigger. Il faut le supprimer puis le recréer différemment.
Par ailleurs, si l'on supprime une table, on supprime également tous les triggers qui y sont attachés.
Exemples
Contraintes et vérification des données
Vérification du sexe des animaux
Dans notre table Animal se trouve la colonne sexe. Cette colonne accepte tout caractère, ou NULL
. Or, seuls les caractères "M" et "F" ont du sens. Nous allons donc créer deux triggers, un pour l'insertion, l'autre pour la modification, qui vont empêcher qu'on donne un autre caractère que "M" ou "F" pour sexe.
Ces deux triggers devront se déclencher avant l'insertion et la modification. On aura donc :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | -- Trigger déclenché par l'insertion DELIMITER | CREATE TRIGGER before_insert_animal BEFORE INSERT ON Animal FOR EACH ROW BEGIN -- Instructions END | -- Trigger déclenché par la modification CREATE TRIGGER before_update_animal BEFORE UPDATE ON Animal FOR EACH ROW BEGIN -- Instructions END | DELIMITER ; |
Il ne reste plus qu'à écrire le code du trigger, qui sera similaire pour les deux triggers. Et comme ce corps contiendra des instructions, il ne faut pas oublier de changer le délimiteur.
Le corps consistera en une simple structure conditionnelle, et définira un comportement à adopter si le sexe donné ne vaut ni "M", ni "F", ni NULL
.
Quel comportement adopter en cas de valeur erronée ?
Deux possibilités :
- on modifie la valeur du sexe, en le mettant à
NULL
par exemple ; - on provoque une erreur, ce qui empêchera l'insertion/la modification.
Commençons par le plus simple : mettre le sexe à NULL
.
1 2 3 4 5 6 7 8 9 10 11 12 | DELIMITER | CREATE TRIGGER before_update_animal BEFORE UPDATE ON Animal FOR EACH ROW BEGIN IF NEW.sexe IS NOT NULL -- le sexe n'est ni NULL AND NEW.sexe != 'M' -- ni "M" AND NEW.sexe != 'F' -- ni "F" THEN SET NEW.sexe = NULL; END IF; END | DELIMITER ; |
Test :
1 2 3 4 5 6 7 | UPDATE Animal SET sexe = 'A' WHERE id = 20; -- l'animal 20 est Balou, un mâle SELECT id, sexe, date_naissance, nom FROM Animal WHERE id = 20; |
id |
sexe |
date_naissance |
nom |
---|---|---|---|
20 |
NULL |
2007-04-24 12:45:00 |
Balou |
Le sexe est bien NULL
, le trigger a fonctionné.
Pour le second trigger, déclenché par l'insertion de lignes, on va implémenter le second comportement : on va déclencher une erreur, ce qui empêchera l'insertion, et affichera l'erreur.
Mais comment déclencher une erreur ?
Contrairement à certains SGBD, MySQL ne dispose pas d'une commande permettant de déclencher une erreur personnalisée. La seule solution est donc de faire une requête dont on sait qu'elle va générer une erreur.
Exemple :
1 | SELECT 1, 2 INTO @a; |
1 | ERROR 1222 (21000): The used SELECT statements have a different number of columns
|
Cependant, il serait quand même intéressant d'avoir un message d'erreur qui soit un peu explicite. Voici une manière d'obtenir un tel message : on crée une table Erreur, ayant deux colonnes, id et erreur. La colonne id est clé primaire, et erreur contient un texte court décrivant l'erreur. Un index UNIQUE
est ajouté sur cette dernière colonne. On insère ensuite une ligne correspondant à l'erreur qu'on veut utiliser dans le trigger.
Ensuite dans le corps du trigger, en cas de valeur erronée, on refait la même insertion. Cela déclenche une erreur de contrainte d'unicité, laquelle affiche le texte qu'on a essayé d'insérer dans Erreur.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | -- Création de la table Erreur CREATE TABLE Erreur ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, erreur VARCHAR(255) UNIQUE); -- Insertion de l'erreur qui nous intéresse INSERT INTO Erreur (erreur) VALUES ('Erreur : sexe doit valoir "M", "F" ou NULL.'); -- Création du trigger DELIMITER | CREATE TRIGGER before_insert_animal BEFORE INSERT ON Animal FOR EACH ROW BEGIN IF NEW.sexe IS NOT NULL -- le sexe n'est ni NULL AND NEW.sexe != 'M' -- ni "M" AND NEW.sexe != 'F' -- ni "F" THEN INSERT INTO Erreur (erreur) VALUES ('Erreur : sexe doit valoir "M", "F" ou NULL.'); END IF; END | DELIMITER ; |
Test :
1 2 | INSERT INTO Animal (nom, sexe, date_naissance, espece_id) VALUES ('Babar', 'A', '2011-08-04 12:34', 3); |
1 | ERROR 1062 (23000): Duplicate entry 'Erreur : sexe doit valoir "M", "F" ou NULL.' for key 'erreur'
|
Et voilà, ce n'est pas parfait, mais au moins le message d'erreur permet de cerner d'où vient le problème. Et Babar n'a pas été inséré.
Vérification du booléen dans Adoption
Il est important de savoir si un client a payé ou non pour les animaux qu'il veut adopter. Il faut donc vérifier la valeur de ce qu'on insère dans la colonne paye, et refuser toute insertion/modification donnant une valeur différente de TRUE
(1) ou FALSE
(0).
Les deux triggers à créer sont très similaires à ce que l'on a fait pour la colonne sexe d'Animal. Essayez donc de construire les requêtes vous-mêmes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | INSERT INTO Erreur (erreur) VALUES ('Erreur : paye doit valoir TRUE (1) ou FALSE (0).'); DELIMITER | CREATE TRIGGER before_insert_adoption BEFORE INSERT ON Adoption FOR EACH ROW BEGIN IF NEW.paye != TRUE -- ni TRUE AND NEW.paye != FALSE -- ni FALSE THEN INSERT INTO Erreur (erreur) VALUES ('Erreur : paye doit valoir TRUE (1) ou FALSE (0).'); END IF; END | CREATE TRIGGER before_update_adoption BEFORE UPDATE ON Adoption FOR EACH ROW BEGIN IF NEW.paye != TRUE -- ni TRUE AND NEW.paye != FALSE -- ni FALSE THEN INSERT INTO Erreur (erreur) VALUES ('Erreur : paye doit valoir TRUE (1) ou FALSE (0).'); END IF; END | DELIMITER ; |
Test :
1 2 3 | UPDATE Adoption SET paye = 3 WHERE client_id = 9; |
1 | ERROR 1062 (23000): Duplicate entry 'Erreur : paye doit valoir TRUE (1) ou FALSE (0)' for key 'erreur'
|
Vérification de la date d'adoption
Il reste une petite chose à vérifier, et ce sera tout pour les vérifications de données : la date d'adoption !
En effet, celle-ci doit être postérieure ou égale à la date de réservation. Un client ne peut pas emporter chez lui un animal avant même d'avoir prévenu qu'il voulait l'adopter.
À nouveau, essayez de faire le trigger vous-mêmes. Pour rappel, il ne peut exister qu'un seul trigger BEFORE UPDATE
et un seul BEFORE INSERT
pour chaque table.
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 30 31 | INSERT INTO Erreur (erreur) VALUES ('Erreur : date_adoption doit être >= à date_reservation.'); DELIMITER | DROP TRIGGER before_insert_adoption| CREATE TRIGGER before_insert_adoption BEFORE INSERT ON Adoption FOR EACH ROW BEGIN IF NEW.paye != TRUE -- On remet la vérification sur paye AND NEW.paye != FALSE THEN INSERT INTO Erreur (erreur) VALUES ('Erreur : paye doit valoir TRUE (1) ou FALSE (0).'); ELSEIF NEW.date_adoption < NEW.date_reservation THEN -- Adoption avant réservation INSERT INTO Erreur (erreur) VALUES ('Erreur : date_adoption doit être >= à date_reservation.'); END IF; END | DROP TRIGGER before_update_adoption| CREATE TRIGGER before_update_adoption BEFORE UPDATE ON Adoption FOR EACH ROW BEGIN IF NEW.paye != TRUE -- On remet la vérification sur paye AND NEW.paye != FALSE THEN INSERT INTO Erreur (erreur) VALUES ('Erreur : paye doit valoir TRUE (1) ou FALSE (0).'); ELSEIF NEW.date_adoption < NEW.date_reservation THEN -- Adoption avant réservation INSERT INTO Erreur (erreur) VALUES ('Erreur : date_adoption doit être >= à date_reservation.'); END IF; END | DELIMITER ; |
On aurait pu faire un second IF
au lieu d'un ELSEIF
, mais de toute façon, le trigger ne pourra déclencher qu'une erreur à la fois.
Test :
1 2 3 4 5 | INSERT INTO Adoption (animal_id, client_id, date_reservation, date_adoption, prix, paye) VALUES (10, 10, NOW(), NOW() - INTERVAL 2 DAY, 200.00, 0); INSERT INTO Adoption (animal_id, client_id, date_reservation, date_adoption, prix, paye) VALUES (10, 10, NOW(), NOW(), 200.00, 4); |
1 2 3 | ERROR 1062 (23000): Duplicate entry 'Erreur : date_adoption doit être >= à date_reservation.' for key 'erreur' ERROR 1062 (23000): Duplicate entry 'Erreur : paye doit valoir TRUE (1) ou FALSE (0).' for key 'erreur' |
Les deux vérifications fonctionnent !
Mise à jour d'informations dépendant d'autres données
Pour l'instant, lorsque l'on a besoin de savoir quels animaux restent disponibles pour l'adoption, il faut faire une requête avec sous-requête.
1 2 3 4 5 6 7 | SELECT id, nom, sexe, date_naissance, commentaires FROM Animal WHERE NOT EXISTS ( SELECT * FROM Adoption WHERE Animal.id = Adoption.animal_id ); |
Mais une telle requête n'est pas particulièrement performante, et elle est relativement peu facile à lire. Les triggers peuvent nous permettre de stocker automatiquement une donnée permettant de savoir immédiatement si un animal est disponible ou non.
Pour cela, il suffit d'ajouter une colonne disponible à la table Animal, qui vaudra FALSE
ou TRUE
, et qui sera mise à jour grâce à trois triggers sur la table Adoption.
- À l'insertion d'une nouvelle adoption, il faut retirer l'animal adopté des animaux disponibles ;
- en cas de suppression, il faut faire le contraire ;
- en cas de modification d'une adoption, si l'animal adopté change, il faut remettre l'ancien parmi les animaux disponibles et retirer le nouveau.
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | -- Ajout de la colonne disponible ALTER TABLE Animal ADD COLUMN disponible BOOLEAN DEFAULT TRUE; -- À l'insertion, un animal est forcément disponible -- Remplissage de la colonne UPDATE Animal SET disponible = FALSE WHERE EXISTS ( SELECT * FROM Adoption WHERE Animal.id = Adoption.animal_id ); -- Création des trois triggers DELIMITER | CREATE TRIGGER after_insert_adoption AFTER INSERT ON Adoption FOR EACH ROW BEGIN UPDATE Animal SET disponible = FALSE WHERE id = NEW.animal_id; END | CREATE TRIGGER after_delete_adoption AFTER DELETE ON Adoption FOR EACH ROW BEGIN UPDATE Animal SET disponible = TRUE WHERE id = OLD.animal_id; END | CREATE TRIGGER after_update_adoption AFTER UPDATE ON Adoption FOR EACH ROW BEGIN IF OLD.animal_id <> NEW.animal_id THEN UPDATE Animal SET disponible = TRUE WHERE id = OLD.animal_id; UPDATE Animal SET disponible = FALSE WHERE id = NEW.animal_id; END IF; END | DELIMITER ; |
Test :
1 2 3 4 | SELECT animal_id, nom, sexe, disponible, client_id FROM Animal INNER JOIN Adoption ON Adoption.animal_id = Animal.id WHERE client_id = 9; |
animal_id |
nom |
sexe |
disponible |
client_id |
---|---|---|---|---|
33 |
Caribou |
M |
0 |
9 |
54 |
Bubulle |
M |
0 |
9 |
55 |
Relou |
M |
0 |
9 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | DELETE FROM Adoption -- 54 doit redevenir disponible WHERE animal_id = 54; UPDATE Adoption SET animal_id = 38, prix = 985.00 -- 38 doit devenir indisponible WHERE animal_id = 33; -- et 33 redevenir disponible INSERT INTO Adoption (client_id, animal_id, date_reservation, prix, paye) VALUES (9, 59, NOW(), 700.00, FALSE); -- 59 doit devenir indisponible SELECT Animal.id AS animal_id, nom, sexe, disponible, client_id FROM Animal LEFT JOIN Adoption ON Animal.id = Adoption.animal_id WHERE Animal.id IN (33, 54, 55, 38, 59); |
animal_id |
nom |
sexe |
disponible |
client_id |
---|---|---|---|---|
33 |
Caribou |
M |
1 |
NULL |
38 |
Boule |
F |
0 |
9 |
54 |
Bubulle |
M |
1 |
NULL |
55 |
Relou |
M |
0 |
9 |
59 |
Bavard |
M |
0 |
9 |
Désormais, pour savoir quels animaux sont disponibles, il suffira de faire la requête suivante :
1 2 3 4 5 6 7 8 9 | SELECT * FROM Animal WHERE disponible = TRUE; -- Ou même SELECT * FROM Animal WHERE disponible; |
Historisation
Voici deux exemples de systèmes d'historisation :
- l'un très basique, gardant simplement trace de l'insertion (date et utilisateur) et de la dernière modification (date et utilisateur), et se faisant directement dans la table concernée ;
- l'autre plus complet, qui garde une copie de chaque version antérieure des lignes dans une table dédiée, ainsi qu'une copie de la dernière version en cas de suppression.
Historisation basique
On va utiliser cette historisation pour la table Race. Libre à vous d'adapter ou de créer les triggers d'autres tables pour les historiser également de cette manière.
On ajoute donc quatre colonnes à la table. Ces colonnes seront toujours remplies automatiquement par les triggers.
1 2 3 4 5 6 7 8 9 10 11 12 | -- On modifie la table Race ALTER TABLE Race ADD COLUMN date_insertion DATETIME, -- date d'insertion ADD COLUMN utilisateur_insertion VARCHAR(20), -- utilisateur ayant inséré la ligne ADD COLUMN date_modification DATETIME, -- date de dernière modification ADD COLUMN utilisateur_modification VARCHAR(20); -- utilisateur ayant fait la dernière modification -- On remplit les colonnes UPDATE Race SET date_insertion = NOW() - INTERVAL 1 DAY, utilisateur_insertion = 'Test', date_modification = NOW()- INTERVAL 1 DAY, utilisateur_modification = 'Test'; |
J'ai mis artificiellement les dates d'insertion et de dernière modification à la veille d'aujourd'hui, et les utilisateurs pour l'insertion et la modification à "Test", afin d'avoir des données intéressantes lors des tests. Idéalement, ce type d'historisation doit bien sûr être mis en place dès la création de la table.
Occupons-nous maintenant des triggers. Il en faut sur l'insertion et sur la modification.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | DELIMITER | CREATE TRIGGER before_insert_race BEFORE INSERT ON Race FOR EACH ROW BEGIN SET NEW.date_insertion = NOW(); SET NEW.utilisateur_insertion = CURRENT_USER(); SET NEW.date_modification = NOW(); SET NEW.utilisateur_modification = CURRENT_USER(); END | CREATE TRIGGER before_update_race BEFORE UPDATE ON Race FOR EACH ROW BEGIN SET NEW.date_modification = NOW(); SET NEW.utilisateur_modification = CURRENT_USER(); END | DELIMITER ; |
Les triggers sont très simples : ils mettent simplement à jour les colonnes d'historisation nécessaires ; ils doivent donc nécessairement être BEFORE
.
Test :
1 2 3 4 5 6 7 8 9 10 | INSERT INTO Race (nom, description, espece_id, prix) VALUES ('Yorkshire terrier', 'Chien de petite taille au pelage long et soyeux de couleur bleu et feu.', 1, 700.00); UPDATE Race SET prix = 630.00 WHERE nom = 'Rottweiller' AND espece_id = 1; SELECT nom, DATE(date_insertion) AS date_ins, utilisateur_insertion AS utilisateur_ins, DATE(date_modification) AS date_mod, utilisateur_modification AS utilisateur_mod FROM Race WHERE espece_id = 1; |
nom |
date_ins |
utilisateur_ins |
date_mod |
utilisateur_mod |
---|---|---|---|---|
Berger allemand |
2012-05-02 |
Test |
2012-05-02 |
Test |
Berger blanc suisse |
2012-05-02 |
Test |
2012-05-02 |
Test |
Rottweiller |
2012-05-02 |
Test |
2012-05-03 |
sdz@localhost |
Yorkshire terrier |
2012-05-03 |
sdz@localhost |
2012-05-03 |
sdz@localhost |
Historisation complète
Nous allons mettre en place un système d'historisation complet pour la table Animal. Celle-ci ne change pas et contiendra la dernière version des données. Par contre, on va ajouter une table Animal_histo, qui contiendra les versions antérieures (quand il y en a) des données d'Animal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | CREATE TABLE Animal_histo ( id SMALLINT(6) UNSIGNED NOT NULL, -- Colonnes historisées sexe CHAR(1), date_naissance DATETIME NOT NULL, nom VARCHAR(30), commentaires TEXT, espece_id SMALLINT(6) UNSIGNED NOT NULL, race_id SMALLINT(6) UNSIGNED DEFAULT NULL, mere_id SMALLINT(6) UNSIGNED DEFAULT NULL, pere_id SMALLINT(6) UNSIGNED DEFAULT NULL, disponible BOOLEAN DEFAULT TRUE, date_histo DATETIME NOT NULL, -- Colonnes techniques utilisateur_histo VARCHAR(20) NOT NULL, evenement_histo CHAR(6) NOT NULL, PRIMARY KEY (id, date_histo) ) ENGINE=InnoDB; |
Les colonnes date_histo et utilisateur_histo contiendront bien sûr la date à laquelle la ligne a été historisée, et l'utilisateur qui a provoqué cette historisation. Quant à la colonne evenement_histo, elle contiendra l'événement qui a déclenché le trigger (soit "DELETE", soit "UPDATE"). La clé primaire de cette table est le couple (id, date_histo).
Voici les triggers nécessaires. Cette fois, ils pourraient être soit BEFORE
, soit AFTER
. Cependant, aucun traitement ne concerne les nouvelles valeurs de la ligne modifiée (ni, a fortiori, de la ligne supprimée). Par conséquent, autant utiliser AFTER
, cela évitera d'exécuter les instructions du trigger en cas d'erreur lors de la requête déclenchant celui-ci.
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | DELIMITER | CREATE TRIGGER after_update_animal AFTER UPDATE ON Animal FOR EACH ROW BEGIN INSERT INTO Animal_histo ( id, sexe, date_naissance, nom, commentaires, espece_id, race_id, mere_id, pere_id, disponible, date_histo, utilisateur_histo, evenement_histo) VALUES ( OLD.id, OLD.sexe, OLD.date_naissance, OLD.nom, OLD.commentaires, OLD.espece_id, OLD.race_id, OLD.mere_id, OLD.pere_id, OLD.disponible, NOW(), CURRENT_USER(), 'UPDATE'); END | CREATE TRIGGER after_delete_animal AFTER DELETE ON Animal FOR EACH ROW BEGIN INSERT INTO Animal_histo ( id, sexe, date_naissance, nom, commentaires, espece_id, race_id, mere_id, pere_id, disponible, date_histo, utilisateur_histo, evenement_histo) VALUES ( OLD.id, OLD.sexe, OLD.date_naissance, OLD.nom, OLD.commentaires, OLD.espece_id, OLD.race_id, OLD.mere_id, OLD.pere_id, OLD.disponible, NOW(), CURRENT_USER(), 'DELETE'); END | DELIMITER ; |
Cette fois, ce sont les valeurs avant modification/suppression qui nous intéressent, d'où l'utilisation de OLD
.
Test :
1 2 3 4 5 6 7 8 9 10 11 12 13 | UPDATE Animal SET commentaires = 'Petit pour son âge' WHERE id = 10; DELETE FROM Animal WHERE id = 47; SELECT id, sexe, date_naissance, nom, commentaires, espece_id FROM Animal WHERE id IN (10, 47); SELECT id, nom, date_histo, utilisateur_histo, evenement_histo FROM Animal_histo; |
id |
sexe |
date_naissance |
nom |
commentaires |
espece_id |
---|---|---|---|---|---|
10 |
M |
2010-07-21 15:41:00 |
Bobo |
Petit pour son âge |
1 |
id |
nom |
date_histo |
utilisateur_histo |
evenement_histo |
---|---|---|---|---|
10 |
Bobo |
2012-05-03 21:51:12 |
sdz@localhost |
UPDATE |
47 |
Scroupy |
2012-05-03 21:51:12 |
sdz@localhost |
DELETE |
Quelques remarques sur l'historisation
Les deux systèmes d'historisation montrés dans ce cours ne sont que deux possibilités parmi des dizaines. Si vous pensez avoir besoin d'un système de ce type, prenez le temps de réfléchir, et de vous renseigner sur les diverses possibilités qui s'offrent à vous. Dans certains systèmes, on combine les deux historisations que j'ai présentées. Parfois, on ne conserve pas les lignes supprimées dans la table d'historisation, mais on utilise plutôt un système d'archive, séparé de l'historisation. Au-delà du modèle d'historisation que vous choisirez, les détails sont également modifiables. Voulez-vous garder toutes les versions des données, ou les garder seulement pour une certaine période de temps ? Voulez-vous enregistrer l'utilisateur SQL ou plutôt des utilisateurs créés pour votre application, découplés des utilisateurs SQL ? Ne restez pas bloqués sur les exemples montrés dans ce cours (que ce soit pour l'historisation ou le reste), le monde est vaste !
Restrictions
Les restrictions sur les triggers sont malheureusement trop importantes pour qu'on puisse se permettre de ne pas les mentionner. On peut espérer qu'une partie de ces restrictions soit levée dans une prochaine version de MySQL, mais en attendant, il est nécessaire d'avoir celles-ci en tête. Voici donc les principales.
Commandes interdites
Il est impossible de travailler avec des transactions à l'intérieur d'un trigger.
Cette restriction est nécessaire, puisque la requête ayant provoqué l'exécution du trigger pourrait très bien se trouver elle-même à l'intérieur d'une transaction. Auquel cas, toute commande START TRANSACTION
, COMMIT
ou ROLLBACK
interagirait avec cette transaction, de manière intempestive.
Les requêtes préparées ne peuvent pas non plus être utilisées.
Enfin, on ne peut pas appeler n'importe quelle procédure à partir d'un trigger.
- Les procédures appelées par un trigger ne peuvent pas envoyer d'informations au client MySQL. Par exemple, elles ne peuvent pas exécuter un simple
SELECT
, qui produit un affichage dans le client (unSELECT...INTO
par contre est permis). Elles peuvent toutefois renvoyer des informations au trigger grâce à des paramètresOUT
ouINOUT
. - Les procédures appelées ne peuvent utiliser ni les transactions (
START TRANSACTION
,COMMIT
ouROLLBACK
) ni les requêtes préparées. C'est-à-dire qu'elles doivent respecter les restrictions des triggers.
Tables utilisées par la requête
Comme mentionné auparavant, il est impossible de modifier les données d'une table utilisée par la requête ayant déclenché le trigger à l'intérieur de celui-ci.
Cette restriction est importante, et peut remettre en question l'utilisation de certains triggers.
Exemple : le trigger AFTER INSERT ON Adoption
modifie les données de la table Animal. Si l'on exécute la requête suivante, cela posera problème.
1 2 3 4 5 6 | INSERT INTO Adoption (animal_id, client_id, date_reservation, prix, paye) SELECT Animal.id, 4, NOW(), COALESCE(Race.prix, Espece.prix), FALSE FROM Animal INNER JOIN Espece ON Espece.id = Animal.espece_id LEFT JOIN Race ON Race.id = Animal.race_id WHERE Animal.nom = 'Boucan' AND Animal.espece_id = 2; |
1 | ERROR 1442 (HY000): Can't update table 'animal' in stored function/trigger because it is already used by statement which invoked this stored function/trigger.
|
Le trigger échoue puisque la table Animal est utilisée par la requête INSERT
qui le déclenche. L'insertion elle-même est donc finalement annulée.
Clés étrangères
Une suppression ou modification de données déclenchée par une clé étrangère ne provoquera pas l'exécution du trigger correspondant.
Par exemple, la colonne Animal.race_id possède une clé étrangère, qui référence la colonne Race.id. Cette clé étrangère a été définie avec l'option ON DELETE SET NULL
. Donc en cas de suppression d'une race, tous les animaux de cette race seront modifiés, et leur race_id changée en NULL
. Il s'agit donc d'une modification de données. Mais comme cette modification a été déclenchée par une contrainte de clé étrangère, les éventuels triggers BEFORE UPDATE
et AFTER UPDATE
de la table Animal ne seront pas déclenchés.
En cas d'utilisation de triggers sur des tables présentant des clés étrangères avec ces options, il vaut donc mieux supprimer celles-ci et déplacer ce comportement dans des triggers. Une autre solution est de ne pas utiliser les triggers sur les tables concernées. Vous pouvez alors remplacer les triggers par l'utilisation de procédures stockées et/ou de transactions.
Qu'avons-nous comme clés étrangères dans nos tables ?
- Race :
CONSTRAINT fk_race_espece_id FOREIGN KEY (espece_id) REFERENCES Espece (id) ON DELETE CASCADE;
- Animal :
CONSTRAINT fk_race_id FOREIGN KEY (race_id) REFERENCES Race (id) ON DELETE SET NULL;
- Animal :
CONSTRAINT fk_espece_id FOREIGN KEY (espece_id) REFERENCES Espece (id);
- Animal :
CONSTRAINT fk_mere_id FOREIGN KEY (mere_id) REFERENCES Animal (id) ON DELETE SET NULL;
- Animal :
CONSTRAINT fk_pere_id FOREIGN KEY (pere_id) REFERENCES Animal (id) ON DELETE SET NULL;
Quatre d'entre elles pourraient donc poser problème. Quatre, sur cinq ! Ce n'est donc pas anodin comme restriction !
On va donc modifier nos clés étrangères pour qu'elles reprennent leur comportement par défaut. Il faudra ensuite créer (ou recréer) quelques triggers pour reproduire le comportement que l'on avait défini.
À ceci près que la restriction sur la modification des données d'une table utilisée par l'événement déclencheur fait qu'on ne pourra pas reproduire certains comportements. On ne pourra pas mettre à NULL
les colonnes pere_id et mere_id de la table Animal en cas de suppression de l'animal de référence.
Voici les commandes :
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 30 | -- On supprime les clés ALTER TABLE Race DROP FOREIGN KEY fk_race_espece_id; ALTER TABLE Animal DROP FOREIGN KEY fk_race_id, DROP FOREIGN KEY fk_mere_id, DROP FOREIGN KEY fk_pere_id; -- On les recrée sans option ALTER TABLE Race ADD CONSTRAINT fk_race_espece_id FOREIGN KEY (espece_id) REFERENCES Espece (id); ALTER TABLE Animal ADD CONSTRAINT fk_race_id FOREIGN KEY (race_id) REFERENCES Race (id), ADD CONSTRAINT fk_mere_id FOREIGN KEY (mere_id) REFERENCES Animal (id), ADD CONSTRAINT fk_pere_id FOREIGN KEY (pere_id) REFERENCES Animal (id); -- Trigger sur Race DELIMITER | CREATE TRIGGER before_delete_race BEFORE DELETE ON Race FOR EACH ROW BEGIN UPDATE Animal SET race_id = NULL WHERE race_id = OLD.id; END| -- Trigger sur Espece CREATE TRIGGER before_delete_espece BEFORE DELETE ON Espece FOR EACH ROW BEGIN DELETE FROM Race WHERE espece_id = OLD.id; END | DELIMITER ; |
En résumé
- Un trigger est un objet stocké dans la base de données, à la manière d'une table ou d'une procédure stockée. La seule différence est qu'un trigger est lié à une table, donc en cas de suppression d'une table, les triggers liés à celle-ci sont supprimés également
- Un trigger définit une ou plusieurs instructions, dont l'exécution est déclenchée par une insertion, une modification ou une suppression de données dans la table à laquelle le trigger est lié.
- Les instructions du trigger peuvent être exécutées avant la requête ayant déclenché celui-ci, ou après. Ce comportement est à définir à la création du trigger.
- Une table ne peut posséder qu'un seul trigger par combinaison événement/moment (
BEFORE UPDATE
,AFTER DELETE
,…) - Les triggers sous MySQL sont soumis à d'importantes (et potentiellement très gênantes) restrictions.