41.10. Funciones disparadoras (triggers) #

41.10.1. Disparadores en cambios de datos
41.10.2. Disparadores de eventos

PL/pgSQL se puede utilizar para definir funciones disparadoras (triggers) sobre cambios de datos o eventos de la base de datos. Una función disparadora se crea con el comando CREATE FUNCTION, declarándola como una función sin argumentos y con un tipo de retorno trigger (para disparadores de cambio de datos) o event_trigger (para disparadores de eventos de base de datos). Se definen automáticamente variables locales especiales denominadas TG_algo para describir la condición que disparó la llamada.

41.10.1. Disparadores en cambios de datos #

Un disparador de cambio de datos se declara como una función sin argumentos y con un tipo de retorno trigger. Ten en cuenta que la función debe declararse sin argumentos incluso si espera recibir algunos argumentos especificados en CREATE TRIGGER; dichos argumentos se pasan a través de TG_ARGV, como se describe a continuación.

Cuando se llama a una función PL/pgSQL como disparador, se crean automáticamente varias variables especiales en el bloque de nivel superior. Estas son:

NEW record #

nueva fila de la base de datos para operaciones INSERT/UPDATE en disparadores a nivel de fila. Esta variable es nula en disparadores a nivel de instrucción y para operaciones DELETE.

OLD record #

antigua fila de la base de datos para operaciones UPDATE/DELETE en disparadores a nivel de fila. Esta variable es nula en disparadores a nivel de instrucción y para operaciones INSERT.

TG_NAME name #

nombre del disparador que se activó.

TG_WHEN text #

BEFORE, AFTER o INSTEAD OF, según la definición del disparador.

TG_LEVEL text #

ROW o STATEMENT, según la definición del disparador.

TG_OP text #

operación para la cual se activó el disparador: INSERT, UPDATE, DELETE o TRUNCATE.

TG_RELID oid (hace referencia a pg_class.oid) #

identificador de objeto de la tabla que causó la invocación del disparador.

TG_RELNAME name #

tabla que causó la invocación del disparador. Esto ahora está en desuso y podría desaparecer en una versión futura. Utiliza TG_TABLE_NAME en su lugar.

TG_TABLE_NAME name #

tabla que causó la invocación del disparador.

TG_TABLE_SCHEMA name #

esquema de la tabla que causó la invocación del disparador.

TG_NARGS integer #

número de argumentos dados a la función disparadora en la instrucción CREATE TRIGGER.

TG_ARGV text[] #

argumentos de la instrucción CREATE TRIGGER. El índice comienza en 0. Los índices inválidos (menores que 0 o mayores o iguales a tg_nargs) dan como resultado un valor nulo.

Una función disparadora debe devolver NULL o un valor de tipo registro/fila que tenga exactamente la estructura de la tabla para la cual se activó el disparador.

Los disparadores a nivel de fila activados BEFORE pueden devolver null para indicarle al gestor de disparadores que omita el resto de la operación para esta fila (es decir, no se activan los disparadores subsiguientes y no ocurre el INSERT/UPDATE/DELETE para esta fila). Si se devuelve un valor no nulo, la operación continúa con ese valor de fila. Devolver un valor de fila diferente del valor original de NEW altera la fila que se insertará o actualizará. Por lo tanto, si la función disparadora desea que la acción desencadenante tenga éxito normalmente sin alterar el valor de la fila, se debe devolver NEW (o un valor equivalente). Para alterar la fila que se va a almacenar, es posible reemplazar valores individuales directamente en NEW y devolver el NEW modificado, o construir un registro/fila completamente nuevo para devolver. En el caso de un disparador before en DELETE, el valor devuelto no tiene un efecto directo, pero debe ser no nulo para permitir que continúe la acción del disparador. Ten en cuenta que NEW es nulo en los disparadores DELETE, por lo que devolverlo no suele ser sensato. El modismo habitual en los disparadores DELETE es devolver OLD.

Los disparadores INSTEAD OF (que siempre son disparadores a nivel de fila y solo se pueden utilizar en vistas) pueden devolver null para indicar que no realizaron ninguna actualización y que se debe omitir el resto de la operación para esta fila (es decir, no se activan los disparadores subsiguientes y la fila no se cuenta en el estado de filas afectadas para el INSERT/UPDATE/DELETE circundante). De lo contrario, se debe devolver un valor no nulo para indicar que el disparador realizó la operación solicitada. Para las operaciones INSERT y UPDATE, el valor devuelto debe ser NEW, que la función disparadora puede modificar para admitir INSERT RETURNING y UPDATE RETURNING (esto también afectará al valor de la fila pasada a cualquier disparador subsiguiente, o pasada a una referencia de alias especial EXCLUDED dentro de una instrucción INSERT con una cláusula ON CONFLICT DO UPDATE). Para las operaciones DELETE, el valor devuelto debe ser OLD.

El valor devuelto de un disparador a nivel de fila activado AFTER o de un disparador a nivel de instrucción activado BEFORE o AFTER siempre se ignora; bien podría ser nulo. Sin embargo, cualquiera de estos tipos de disparadores aún podría abortar la operación completa lanzando un error.

La Example 41.3 muestra un ejemplo de una función disparadora en PL/pgSQL.

Example 41.3. Una función disparadora de PL/pgSQL

Este disparador de ejemplo asegura que cada vez que se inserta o actualiza una fila en la tabla, el nombre del usuario actual y la hora se graben en la fila. Y comprueba que se proporcione el nombre del empleado y que el salario sea un valor positivo.

CREATE TABLE emp (
    empname           text,
    salary            integer,
    last_date         timestamp,
    last_user         text
);

CREATE FUNCTION emp_stamp() RETURNS trigger AS $emp_stamp$
    BEGIN
        -- Comprobar que se proporcionen empname y salary
        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én trabaja para nosotros cuando tiene que pagar por ello?
        IF NEW.salary < 0 THEN
            RAISE EXCEPTION '% cannot have a negative salary', NEW.empname;
        END IF;

        -- Recordar quién cambió la nómina y cuándo
        NEW.last_date := current_timestamp;
        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 FUNCTION emp_stamp();

Otra forma de registrar cambios en una tabla consiste en crear una nueva tabla que contenga una fila por cada inserción, actualización o eliminación que ocurra. Este enfoque se puede considerar como una auditoría de cambios en una tabla. La Example 41.4 muestra un ejemplo de una función disparadora de auditoría en PL/pgSQL.

Example 41.4. Una función disparadora de PL/pgSQL para auditoría

Este disparador de ejemplo asegura que cualquier inserción, actualización o eliminación de una fila en la tabla emp se registre (es decir, se audite) en la tabla emp_audit. La hora actual y el nombre de usuario se graben en la fila, junto con el tipo de operación realizada en ella.

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
        --
        -- Crear una fila en emp_audit para reflejar la operación realizada en emp,
        -- haciendo uso de la variable especial TG_OP para determinar la operación.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit SELECT 'D', now(), current_user, OLD.*;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit SELECT 'U', now(), current_user, NEW.*;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit SELECT 'I', now(), current_user, NEW.*;
        END IF;
        RETURN NULL; -- result is ignored since this is an AFTER trigger
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
AFTER INSERT OR UPDATE OR DELETE ON emp
    FOR EACH ROW EXECUTE FUNCTION process_emp_audit();

Una variación del ejemplo anterior utiliza una vista que une la tabla principal con la tabla de auditoría, para mostrar cuándo se modificó por última vez cada entrada. Este enfoque sigue registrando el historial de auditoría completo de los cambios en la tabla, pero también presenta una vista simplificada del historial de auditoría, mostrando solo la última marca de tiempo modificada derivada del historial de auditoría para cada entrada. La Example 41.5 muestra un ejemplo de un disparador de auditoría en una vista en PL/pgSQL.

Example 41.5. Una función disparadora de vista de PL/pgSQL para auditoría

Este ejemplo utiliza un disparador en la vista para hacerla actualizable y asegurar que cualquier inserción, actualización o eliminación de una fila en la vista se registre (es decir, se audite) en la tabla emp_audit. Se registran la hora actual y el nombre de usuario, junto con el tipo de operación realizada, y la vista muestra la hora de la última modificación de cada fila.

CREATE TABLE emp (
    empname           text PRIMARY KEY,
    salary            integer
);

CREATE TABLE emp_audit(
    operation         char(1)   NOT NULL,
    userid            text      NOT NULL,
    empname           text      NOT NULL,
    salary            integer,
    stamp             timestamp NOT NULL
);

CREATE VIEW emp_view AS
    SELECT e.empname,
           e.salary,
           max(ea.stamp) AS last_updated
      FROM emp e
      LEFT JOIN emp_audit ea ON ea.empname = e.empname
     GROUP BY 1, 2;

CREATE OR REPLACE FUNCTION update_emp_view() RETURNS TRIGGER AS $$
    BEGIN
        --
        -- Realizar la operación requerida en emp y crear una fila en emp_audit
        -- para reflejar el cambio realizado en emp.
        --
        IF (TG_OP = 'DELETE') THEN
            DELETE FROM emp WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            OLD.last_updated = now();
            INSERT INTO emp_audit VALUES('D', current_user, OLD.*);
            RETURN OLD;
        ELSIF (TG_OP = 'UPDATE') THEN
            UPDATE emp SET salary = NEW.salary WHERE empname = OLD.empname;
            IF NOT FOUND THEN RETURN NULL; END IF;

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('U', current_user, NEW.*);
            RETURN NEW;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp VALUES(NEW.empname, NEW.salary);

            NEW.last_updated = now();
            INSERT INTO emp_audit VALUES('I', current_user, NEW.*);
            RETURN NEW;
        END IF;
    END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit
INSTEAD OF INSERT OR UPDATE OR DELETE ON emp_view
    FOR EACH ROW EXECUTE FUNCTION update_emp_view();

Un uso de los disparadores es mantener una tabla de resumen de otra tabla. El resumen resultante se puede utilizar en lugar de la tabla original para ciertas consultas, a menudo con tiempos de ejecución enormemente reducidos. Esta técnica se utiliza comúnmente en los almacenes de datos (Data Warehousing), donde las tablas de datos medidos u observados (llamadas tablas de hechos) pueden ser extremadamente grandes. La Example 41.6 muestra un ejemplo de una función disparadora en PL/pgSQL que mantiene una tabla de resumen para una tabla de hechos en un almacén de datos.

Example 41.6. Una función disparadora de PL/pgSQL para mantener una tabla de resumen

El esquema detallado aquí se basa en parte en el ejemplo de la tienda de comestibles de The Data Warehouse Toolkit por Ralph Kimball.

--
-- Tablas principales - dimensión de tiempo y hechos de ventas.
--
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);

--
-- Tabla de resumen - ventas por tiempo.
--
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);

--
-- Función y disparador para modificar la(s) columna(s) resumida(s) en 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

        -- Determinar la(s) cantidad(es) de incremento/decremento.
        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

            -- prohibir actualizaciones que cambien el time_key -
            -- (probablemente no sea demasiado oneroso, ya que DELETE + INSERT es como se
            -- realizarán la mayoría de los cambios).
            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;


        -- Insertar o actualizar la fila de resumen con los nuevos valores.
        <<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
                    -- no hacer nada
            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 FUNCTION 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;

Los disparadores AFTER también pueden hacer uso de tablas de transición para inspeccionar el conjunto completo de filas cambiadas por la instrucción desencadenante. El comando CREATE TRIGGER asigna nombres a una o ambas tablas de transición, y luego la función puede referirse a esos nombres como si fueran tablas temporales de solo lectura. La Example 41.7 muestra un ejemplo.

Example 41.7. Auditoría con tablas de transición

Este ejemplo produce los mismos resultados que la Example 41.4, pero en lugar de utilizar un disparador que se activa para cada fila, utiliza un disparador que se activa una vez por instrucción, después de recopilar la información relevante en una tabla de transición. Esto puede ser significativamente más rápido que el enfoque de disparador por fila cuando la instrucción invocadora ha modificado muchas filas. Ten en cuenta que debemos realizar una declaración de disparador separada para cada tipo de evento, ya que las cláusulas REFERENCING deben ser diferentes para cada caso. Pero esto no nos impide utilizar una única función disparadora si así lo decidimos. (En la práctica, podría ser mejor utilizar tres funciones separadas y evitar las pruebas en tiempo de ejecución sobre TG_OP).

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
        --
        -- Crear filas en emp_audit para reflejar las operaciones realizadas en emp,
        -- haciendo uso de la variable especial TG_OP para determinar la operación.
        --
        IF (TG_OP = 'DELETE') THEN
            INSERT INTO emp_audit
                SELECT 'D', now(), current_user, o.* FROM old_table o;
        ELSIF (TG_OP = 'UPDATE') THEN
            INSERT INTO emp_audit
                SELECT 'U', now(), current_user, n.* FROM new_table n;
        ELSIF (TG_OP = 'INSERT') THEN
            INSERT INTO emp_audit
                SELECT 'I', now(), current_user, n.* FROM new_table n;
        END IF;
        RETURN NULL; -- el resultado se ignora ya que es un disparador AFTER
    END;
$emp_audit$ LANGUAGE plpgsql;

CREATE TRIGGER emp_audit_ins
    AFTER INSERT ON emp
    REFERENCING NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_upd
    AFTER UPDATE ON emp
    REFERENCING OLD TABLE AS old_table NEW TABLE AS new_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();
CREATE TRIGGER emp_audit_del
    AFTER DELETE ON emp
    REFERENCING OLD TABLE AS old_table
    FOR EACH STATEMENT EXECUTE FUNCTION process_emp_audit();

41.10.2. Disparadores de eventos #

PL/pgSQL se puede utilizar para definir disparadores de eventos. PostgreSQL requiere que una función que se vaya a llamar como disparador de eventos se declare como una función sin argumentos y con un tipo de retorno event_trigger.

Cuando se llama a una función PL/pgSQL como disparador de eventos, se crean automáticamente varias variables especiales en el bloque de nivel superior. Estas son:

TG_EVENT text #

evento para el cual se activa el disparador.

TG_TAG text #

etiqueta del comando para el cual se activa el disparador.

La Example 41.8 muestra un ejemplo de una función disparadora de eventos en PL/pgSQL.

Example 41.8. Una función disparadora de eventos de PL/pgSQL

Este disparador de ejemplo simplemente lanza un mensaje NOTICE cada vez que se ejecuta un comando admitido.

CREATE OR REPLACE FUNCTION snitch() RETURNS event_trigger AS $$
BEGIN
    RAISE NOTICE 'snitch: % %', tg_event, tg_tag;
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER snitch ON ddl_command_start EXECUTE FUNCTION snitch();