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_ para describir la condición
que disparó la llamada.
algo
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();
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:
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();