PL/pgSQL peut être utilisé pour définir des procédures trigger. Une procédure trigger est créée grâce à la commande CREATE FUNCTION utilisée comme fonction sans arguments ayant un type de retour trigger. Notez que la fonction doit être déclarée avec aucun argument même si elle s'attend à recevoir les arguments spécifiés dans CREATE TRIGGER -- les arguments trigger sont passés via TG_ARGV, comme décrit plus loin.
Quand une fonction PL/pgSQL est appelée en tant que trigger, plusieurs variables spéciales sont créées automatiquement dans le bloc de plus haut niveau. Ce sont :
Type de données RECORD ; variable contenant la nouvelle ligne de base de données pour les opérations INSERT/UPDATE dans les triggers de niveau ligne. Cette variable est NULL dans un trigger de niveau instruction et pour les opérations DELETE.
Type de données RECORD ; variable contenant l'ancienne ligne de base de données pour les opérations UPDATE/DELETE dans les triggers de niveau ligne. Cette variable est NULL dans les triggers de niveau instruction et pour les opérations INSERT.
Type de données name ; variable qui contient le nom du trigger réellement lancé.
Type de données text ; une chaîne, soit BEFORE soit AFTER, soit INSTEAD OF selon la définition du trigger.
Type de données text ; une chaîne, soit ROW soit STATEMENT, selon la définition du trigger.
Type de données text ; une chaîne, INSERT, UPDATE, DELETE ou TRUNCATE indiquant pour quelle opération le trigger a été lancé.
Type de données oid ; l'ID de l'objet de la table qui a causé le déclenchement du trigger.
Type de données name ; le nom de la table qui a causé le déclenchement. C'est obsolète et pourrait disparaître dans une prochaine version. À la place, utilisez TG_TABLE_NAME.
Type de données name ; le nom de la table qui a déclenché le trigger.
Type de données name ; le nom du schéma de la table qui a appelé le trigger.
Type de données integer ; le nombre d'arguments donnés à la procédure trigger dans l'instruction CREATE TRIGGER.
Type de donnée text ; les arguments de l'instruction CREATE TRIGGER. L'index débute à 0. Les indices invalides (inférieurs à 0 ou supérieurs ou égaux à tg_nargs) auront une valeur NULL.
Une fonction trigger doit renvoyer soit NULL soit une valeur record ayant exactement la structure de la table pour laquelle le trigger a été lancé.
Les triggers de niveau ligne lancés BEFORE peuvent renvoyer NULL pour indiquer au gestionnaire de trigger de sauter le reste de l'opération pour cette ligne (les triggers suivants ne sont pas lancés, et les INSERT/UPDATE/DELETE ne se font pas pour cette ligne). Si une valeur non NULL est renvoyée alors l'opération se déroule avec cette valeur ligne. Renvoyer une valeur ligne différente de la valeur originale de NEW modifie la ligne qui sera insérée ou mise à jour. De ce fait, si la fonction de trigger veut que l'action réussise sans modifier la valeur de rangée, NEW (ou une valeur égale) doit être renvoyée. Pour modifier la rangée à être stockée, il est possible de remplacer les valeurs directement dans NEW et renvoyer le NEW modifié ou de générer un nouvel enregistrement à renvoyer. Dans le cas d'un before-trigger sur une commande DELETE, la valeur renvoyée n'a aucun effet direct mais doit être non-nulle pour permettre à l'action trigger de continuer. Notez que NEW est nul dans le cadre des triggers DELETE et que renvoyer ceci n'est pas recommandé dans les cas courants. Une pratique utile dans des triggers DELETE serait de renvoyer OLD.
Les triggers INSTEAD OF (qui sont toujours des triggers au niveau ligne et peuvent seulement être utilisés sur des vues) peuvent renvoyer NULL pour signaler qu'ils n'ont fait aucune modification et que le reste de l'opération pour cette ligne doit être ignoré (autrement dit, les triggers suivants ne sont pas déclenchés et la ligne n'est pas comptée dans le statut des lignes affectées pour la requête INSERT/UPDATE/DELETE). Une valeur différente de NULL doit être renvoyée pour indiquer que le trigger a traité l'opération demandée. Pour les opérations INSERT et UPDATE, la valeur de retour doit être NEW, que la fonction trigger peut modifier pour supporter une clause RETURNING d'une requête INSERT ou UPDATE (cela affectera aussi la valeur de la ligne passée aux autres triggers). Pour les requêtes DELETE, la valeur de retour doit être OLD.
La valeur de retour d'un trigger de niveau rangée déclenché AFTER ou un trigger de niveau instruction déclenché BEFORE ou AFTER est toujours ignoré ; il pourrait aussi bien être NULL. Néanmoins, tous les types de triggers peuvent toujours annuler l'opération complète en envoyant une erreur.
L'Exemple 39.3, « Une procédure trigger PL/pgSQL » montre un exemple d'une procédure trigger dans PL/pgSQL.
Exemple 39.3. Une procédure trigger PL/pgSQL
Cet exemple de trigger assure qu'à chaque moment où une ligne est insérée ou mise à jour dans la table, le nom de l'utilisateur courant et l'heure sont estampillés dans la ligne. Et cela vous assure qu'un nom d'employé est donné et que le salaire est une valeur positive.
CREATE TABLE emp ( nom_employe text, salaire integer, date_dermodif timestamp, utilisateur_dermodif text ); CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$ BEGIN -- Verifie que nom_employe et salary sont donnés IF NEW.nom_employe IS NULL THEN RAISE EXCEPTION 'nom_employe ne peut pas être NULL'; END IF; IF NEW.salaire IS NULL THEN RAISE EXCEPTION '% ne peut pas avoir un salaire', NEW.nom_employe; END IF; -- Qui travaille pour nous si la personne doit payer pour cela ? IF NEW.salaire < 0 THEN RAISE EXCEPTION '% ne peut pas avoir un salaire négatif', NEW.nom_employe; END IF; -- Rappelons-nous qui a changé le salaire et quand NEW.date_dermodif := current_timestamp; NEW.utilisateur_dermodif := current_user; RETURN NEW; END; $emp_stamp$ LANGUAGE plpgsql; CREATE TRIGGER emp_stamp BEFORE INSERT OR UPDATE ON emp FOR EACH ROW EXECUTE PROCEDURE emp_stamp();
Une autre façon de tracer les modifications sur une table implique la création d'une nouvelle table qui contient une ligne pour chaque insertion, mise à jour ou suppression qui survient. Cette approche peut être vue comme un audit des modifications sur une table. L'Exemple 39.4, « Une procédure d'audit par trigger en PL/pgSQL » montre un exemple d'une procédure d'audit par trigger en PL/pgSQL.
Exemple 39.4. Une procédure d'audit par trigger en PL/pgSQL
Cet exemple de trigger nous assure que toute insertion, modification ou suppression d'une ligne dans la table emp est enregistrée dans la table emp_audit. L'heure et le nom de l'utilisateur sont conservées dans la ligne avec le type d'opération réalisé.
CREATE TABLE emp ( nom_employe text NOT NULL, salaire integer ); CREATE TABLE emp_audit( operation char(1) NOT NULL, tampon timestamp NOT NULL, id_utilisateur text NOT NULL, nom_employe text NOT NULL, salaire integer ); CREATE OR REPLACE FUNCTION audit_employe() RETURNS TRIGGER AS $emp_audit$ BEGIN -- -- Ajoute une ligne dans emp_audit pour refléter l'opération réalisée -- sur emp, -- utilise la variable spéciale TG_OP pour cette opération. -- IF (TG_OP = 'DELETE') THEN INSERT INTO emp_audit SELECT 'D', now(), user, OLD.*; RETURN OLD; ELSIF (TG_OP = 'UPDATE') THEN INSERT INTO emp_audit SELECT 'U', now(), user, NEW.*; RETURN NEW; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO emp_audit SELECT 'I', now(), user, NEW.*; RETURN NEW; END IF; RETURN NULL; -- le résultat est ignoré car il s'agit d'un trigger AFTER END; $emp_audit$ language plpgsql; CREATE TRIGGER emp_audit AFTER INSERT OR UPDATE OR DELETE ON emp FOR EACH ROW EXECUTE PROCEDURE audit_employe();
Une variation de l'exemple précédent utilise une vue joignant la table principale et la table d'audit pour montrer les derniers enregistrements modifiés. Cette approche enregistre toujours toutes les modifications sur la table mais présente aussi une vue simple de l'audit, n'affichant que le date et heure de la dernière modification pour chaque enregistrement. Exemple 39.5, « Une fonction trigger en PL/pgSQL surune vue pour un audit » montre un exemple d'un trigger d'audit sur une vue avec PL/pgSQL.
Exemple 39.5. Une fonction trigger en PL/pgSQL surune vue pour un audit
Cet exemple utilise un trigger sur une vue pour la rendre modifiable, et s'assure que toute insertion, mise à jour ou suppression d'une ligne dans la vue est enregistrée (pour l'audit) dans la table emp_audit. La date et l'heure courante ainsi que le nom de l'utilisateur sont enregistrés, avec le type d'opération réalisé pour que la vue affiche la date et l'heure de la dernière modification de chaque ligne.
CREATE TABLE emp ( nom_employe text PRIMARY KEY, salaire integer ); CREATE TABLE emp_audit( operation char(1) NOT NULL, id_utilisateur text NOT NULL, nom_employe text NOT NULL, salaire integer, dmodif timestamp NOT NULL ); CREATE VIEW emp_vue AS SELECT e.nom_employe, e.salaire, max(ea.dmodif) AS derniere_modification FROM emp e LEFT JOIN emp_audit ea ON ea.nom_employe = e.nom_employe GROUP BY 1, 2; CREATE OR REPLACE FUNCTION miseajour_emp_vue() RETURNS TRIGGER AS $$ BEGIN -- -- Perform the required operation on emp, and create a row in emp_audit -- to reflect the change made to emp. -- IF (TG_OP = 'DELETE') THEN DELETE FROM emp WHERE nom_employe = OLD.nom_employe; IF NOT FOUND THEN RETURN NULL; END IF; OLD.derniere_modification = now(); INSERT INTO emp_audit VALUES('D', user, OLD.*); RETURN OLD; ELSIF (TG_OP = 'UPDATE') THEN UPDATE emp SET salary = NEW.salary WHERE nom_employe = OLD.nom_employe; IF NOT FOUND THEN RETURN NULL; END IF; NEW.derniere_modification = now(); INSERT INTO emp_audit VALUES('U', user, NEW.*); RETURN NEW; ELSIF (TG_OP = 'INSERT') THEN INSERT INTO emp VALUES(NEW.nom_employe, NEW.salaire); NEW.derniere_modification = now(); INSERT INTO emp_audit VALUES('I', user, NEW.*); RETURN NEW; END IF; END; $$ LANGUAGE plpgsql; CREATE TRIGGER emp_audit INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_vue FOR EACH ROW EXECUTE PROCEDURE miseajour_emp_vue();
Une utilisation des triggers est le maintien d'une table résumée d'une autre table. Le résumé résultant peut être utilisé à la place de la table originale pour certaines requêtes -- souvent avec des temps d'exécution bien réduits. Cette technique est souvent utilisée pour les statistiques de données où les tables de données mesurées ou observées (appelées des tables de faits) peuvent être extrêmement grandes. L'Exemple 39.6, « Une procédure trigger PL/pgSQL pour maintenir une table résumée » montre un exemple d'une procédure trigger en PL/pgSQL maintenant une table résumée pour une table de faits dans un système de données (data warehouse).
Exemple 39.6. Une procédure trigger PL/pgSQL pour maintenir une table résumée
Le schéma détaillé ici est partiellement basé sur l'exemple du Grocery Store provenant de The Data Warehouse Toolkit par Ralph Kimball.
-- -- Tables principales - dimension du temps de ventes. -- CREATE TABLE time_dimension ( time_key integer NOT NULL, day_of_week integer NOT NULL, day_of_month integer NOT NULL, month integer NOT NULL, quarter integer NOT NULL, year integer NOT NULL ); CREATE UNIQUE INDEX time_dimension_key ON time_dimension(time_key); CREATE TABLE sales_fact ( time_key integer NOT NULL, product_key integer NOT NULL, store_key integer NOT NULL, amount_sold numeric(12,2) NOT NULL, units_sold integer NOT NULL, amount_cost numeric(12,2) NOT NULL ); CREATE INDEX sales_fact_time ON sales_fact(time_key); -- -- Table résumé - ventes sur le temps. -- CREATE TABLE sales_summary_bytime ( time_key integer NOT NULL, amount_sold numeric(15,2) NOT NULL, units_sold numeric(12) NOT NULL, amount_cost numeric(15,2) NOT NULL ); CREATE UNIQUE INDEX sales_summary_bytime_key ON sales_summary_bytime(time_key); -- -- Fonction et trigger pour amender les colonnes résumées -- pour un UPDATE, INSERT, DELETE. -- CREATE OR REPLACE FUNCTION maint_sales_summary_bytime() RETURNS TRIGGER AS $maint_sales_summary_bytime$ DECLARE delta_time_key integer; delta_amount_sold numeric(15,2); delta_units_sold numeric(12); delta_amount_cost numeric(15,2); BEGIN -- Travaille sur l'ajout/la suppression de montant(s). IF (TG_OP = 'DELETE') THEN delta_time_key = OLD.time_key; delta_amount_sold = -1 * OLD.amount_sold; delta_units_sold = -1 * OLD.units_sold; delta_amount_cost = -1 * OLD.amount_cost; ELSIF (TG_OP = 'UPDATE') THEN -- interdit les mises à jour qui modifient time_key - -- (probablement pas trop cher, car DELETE + INSERT est la façon la plus -- probable de réaliser les modifications). IF ( OLD.time_key != NEW.time_key) THEN RAISE EXCEPTION 'Update of time_key : % -> % not allowed', OLD.time_key, NEW.time_key; END IF; delta_time_key = OLD.time_key; delta_amount_sold = NEW.amount_sold - OLD.amount_sold; delta_units_sold = NEW.units_sold - OLD.units_sold; delta_amount_cost = NEW.amount_cost - OLD.amount_cost; ELSIF (TG_OP = 'INSERT') THEN delta_time_key = NEW.time_key; delta_amount_sold = NEW.amount_sold; delta_units_sold = NEW.units_sold; delta_amount_cost = NEW.amount_cost; END IF; -- Insertion ou mise à jour de la ligne de résumé avec les nouvelles valeurs. <<insert_update>> LOOP UPDATE sales_summary_bytime SET amount_sold = amount_sold + delta_amount_sold, units_sold = units_sold + delta_units_sold, amount_cost = amount_cost + delta_amount_cost WHERE time_key = delta_time_key; EXIT insert_update WHEN found; BEGIN INSERT INTO sales_summary_bytime ( time_key, amount_sold, units_sold, amount_cost) VALUES ( delta_time_key, delta_amount_sold, delta_units_sold, delta_amount_cost ); EXIT insert_update; EXCEPTION WHEN UNIQUE_VIOLATION THEN -- do nothing END; END LOOP insert_update; RETURN NULL; END; $maint_sales_summary_bytime$ LANGUAGE plpgsql; CREATE TRIGGER maint_sales_summary_bytime AFTER INSERT OR UPDATE OR DELETE ON sales_fact FOR EACH ROW EXECUTE PROCEDURE maint_sales_summary_bytime(); INSERT INTO sales_fact VALUES(1,1,1,10,3,15); INSERT INTO sales_fact VALUES(1,2,1,20,5,35); INSERT INTO sales_fact VALUES(2,2,1,40,15,135); INSERT INTO sales_fact VALUES(2,3,1,10,1,13); SELECT * FROM sales_summary_bytime; DELETE FROM sales_fact WHERE product_key = 1; SELECT * FROM sales_summary_bytime; UPDATE sales_fact SET units_sold = units_sold * 2; SELECT * FROM sales_summary_bytime;