35.10. Procédures déclencheur

PL/pgSQL peut être utilisé pour définir des procédures déclencheur. Une procédure déclencheur 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 déclencheurs de niveau ligne. Cette variable est NULL dans un déclencheur de niveau instruction.

OLD

Type de données RECORD ; variable contenant l'ancienne ligne de base de données pour les opérations UPDATE/DELETE dans les déclencheurs de niveau ligne. Cette variable est NULL dans les déclencheurs de niveau instruction.

TG_NAME

Type de données nom ; variable qui contient le nom du déclencheur réellement lancé.

TG_WHEN

Type de données text ; une chaîne, soit BEFORE soit AFTER selon la définition du déclencheur.

TG_LEVEL

Type de données text ; une chaîne, soit ROW soit STATEMENT selon la définition du déclencheur.

TG_OP

Type de données text ; une chaîne, INSERT, UPDATE ou DELETE, indiquant pour quelle opération le déclencheur a été lancé.

TG_RELID

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

TG_RELNAME

Type de données nom ; le nom de la table qui a causé le déclenchement.

TG_NARGS

Type de données integer ; le nombre d'arguments donnés à la procédure déclencheur 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 déclencheur doit renvoyer soit NULL soit une valeur record/ligne ayant exactement la structure de la table pour laquelle le déclencheur a été lancé.

Les déclencheurs de niveau ligne lancés BEFORE peuvent renvoyer NULL pour indiquer au gestionnaire de déclencheur de sauter le reste de l'opération pour cette ligne (les déclencheurs 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 (mais n'a pas d'effet sur le cas DELETE). Pour modifier la ligne à stocker, il est possible de remplacer des valeurs seules directement dans NEW et de renvoyer NEW, ou de construire un nouveau record/ligne à renvoyer.

La valeur de retour d'un déclencheur de niveau ligne BEFORE ou AFTER ou un déclencheur de niveau ligne AFTER est toujours ignoré ; il pourrait aussi bien être NULL. Néanmoins, tous les types de déclencheurs peuvent toujours annuler l'opération complète en envoyant une erreur.

L'Exemple 35-2 montre un exemple d'une procédure déclencheur dans PL/pgSQL.

Exemple 35-2. Une procédure déclencheur PL/pgSQL

Cet exemple de déclencheur 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 (
    empname text,
    salary integer,
    last_date timestamp,
    last_user text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Verifie que empname et salary sont donnés
        IF NEW.empname IS NULL THEN
            RAISE EXCEPTION 'empname cannot be NULL';
        END IF;
        IF NEW.salary IS NULL THEN
            RAISE EXCEPTION '% cannot have NULL salary', NEW.empname;
        END IF;

        -- Qui travaille pour nous quand elle doit payer pour cela ?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Rappelons nous qui a changé le payroll quand 
        NEW.last_date := 'now';
        NEW.last_user := 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 35-3 montre un exemple d'une procédure d'audit par déclencheur en PL/pgSQL.

Exemple 35-3. Une procédure d'audit par déclencheur en PL/pgSQL

Cet exemple de déclencheur 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 (
    empname           text NOT NULL,
    salary            integer
);
        
CREATE TABLE emp_audit( 
    operation         char(1)   NOT NULL,
    stamp             timestamp NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary integer
);
        
CREATE OR REPLACE FUNCTION process_emp_audit() 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 déclencheur AFTER
END;
$emp_audit$ language plpgsql;
        
CREATE TRIGGER emp_audit
    AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE PROCEDURE process_emp_audit();

Une utilisation des déclencheurs 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 35-4 montre un exemple d'une procédure déclencheur en PL/pgSQL maintenant une table résumée pour une table de faits dans un système de données (data warehouse).

Exemple 35-4. Une procédure déclencheur 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 déclencheur 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;
          
          
  -- Mise à jour de la ligne de résumé avec les nouvelles valeurs.
  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;
          
          
  -- Il pourrait n'y avoir aucune ligne pour ce time_key
  -- (donc nouvelle donnée!).
  IF (NOT FOUND) THEN
    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
        );
    EXCEPTION
      --
      -- Récupère le cas où deux transactions ajoutent des données pour un
      -- nouveau time_key.
      --
      WHEN UNIQUE_VIOLATION THEN
        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;
          
        END;
  END IF;
  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();