IdentifiantMot de passe
Loading...
Mot de passe oubli� ?Je m'inscris ! (gratuit)

39.9. Proc�dures trigger

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 :

NEW

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.

OLD

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.

TG_NAME

Type de donn�es name ; variable qui contient le nom du trigger r�ellement lanc�.

TG_WHEN

Type de donn�es text ; une cha�ne, soit BEFORE soit AFTER, soit INSTEAD OF selon la d�finition du trigger.

TG_LEVEL

Type de donn�es text ; une cha�ne, soit ROW soit STATEMENT, selon la d�finition du trigger.

TG_OP

Type de donn�es text ; une cha�ne, INSERT, UPDATE, DELETE ou TRUNCATE indiquant pour quelle op�ration le trigger a �t� lanc�.

TG_RELID

Type de donn�es oid ; l'ID de l'objet de la table qui a caus� le d�clenchement du trigger.

TG_RELNAME

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.

TG_TABLE_NAME

Type de donn�es name ; le nom de la table qui a d�clench� le trigger.

TG_TABLE_SCHEMA

Type de donn�es name ; le nom du sch�ma de la table qui a appel� le trigger.

TG_NARGS

Type de donn�es integer ; le nombre d'arguments donn�s � la proc�dure trigger dans l'instruction CREATE TRIGGER.

TG_ARGV[]

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;