[PostgreSQL] Crear triggers dinámicos con postgreSQL

¿Qué es un trigger dinámico?, es aquel trigger que la metadata de en cual tabla/campo va a operar es pasado de manera dinámica (parámetros) a la función de trigger (trigger function).

¿Y para qué sirve?, bueno, imaginen un sistema dónde cada operación debe ser por ejemplo, agregada a una tabla auditoría, o por ejemplo, que una operación en una tabla, causa una operación en otra tabla, que aunque el código sea *casi* el mismo, depende de la tabla que dispara el trigger, qué tabla va a operar.

La utilidad de esto es simplemente poder re-utilizar una única funcion trigger para diversas acciones a ser tomadas de acuerdo a los parámetros recibidos.

El ejemplo

Este ejemplo se construyó durante un taller de postgreSQL que estaba dictando, el criterio era el siguiente:

  • El trigger toma el valor insertado en una tabla
  • Busca dicho valor en otra tabla, usando otro campo como criterio
  • Si el valor no existe, retorna NULL
  • Si el valor existe, retorna el objeto NEW del disparador, con lo cual la inserción sí ocurre

Los campos TG_*

En los disparadores de postgreSQL se cuenta con un conjunto de variables muy útiles, como son:

  • TG_OP: define la operación realizada en la tabla (INSERT, UPDATE o DELETE)
  • TG_TABLE_SCHEMA: define el schema al que pertenece la tabla (ej: “public”)
  • TG_TABLE_NAME: define el nombre de la tabla que disparó este trigger
  • TG_ARGV: es un arreglo (array []) que contiene los parámetros enviados a una función de trigger.

Con esto podemos hacer cualquier tipo de condicional en la función de trigger, reduciendo el código a sólo una función que se usará, de acuerdo a los parámetros que se le pasen.

La Función

CREATE OR REPLACE FUNCTION permitir_insert() RETURNS trigger AS

Comenzamos con la definición, la llamaremos “permitir_insert()” y obviamente, retorna un trigger.

luego, en la sección de declaración, definimos las únicas variables que vamos a necesitar para este ejemplo:

 vrecord integer;
campo text;
tabla text;
id int;
rc int;

BEGIN

En el cuerpo de la función, preguntamos qué tabla y campo voy a consultar para verificar si permito o no el insert, esto se hace consultando a TG_ARGV:

 tabla := TG_ARGV[0];
campo := TG_ARGV[1];

Como ven, ya que podamos recibir cualquier cantidad de parámetros en el trigger, las opciones son infinitas.

Ahora, necesito saber que hay en el campo trigger (NEW.campo), en este caso, debo reemplazar “campo” con el nombre del campo de la tabla que disparó el trigger (y que voy a consultar en otra tabla), como en mi caso el valor de el “campo” en todas las “tablas” que usarán esta función de trigger es entero, he declarado “vrecord” integer, si su tipo será distinto, sería conveniente declararlo “anyelement”.

EXECUTE 'SELECT ($1).' || quote_ident(campo) || '::text' INTO vrecord USING NEW;

Lo que hace ese código, es ejecutar un $1.”campo”::text (dónde campo es una variable) y mete su valor en la variable “vrecord”, se usa el objeto NEW para la sustitución del $1.

Ejemplo, si el campo se llama “piezas“, ese “EXECUTE” quedaría “NEW.piezas” y entonces metería el valor de NEW.piezas en la variable “vrecord”.

¿sencillo, no?.

La consulta

Ahora, debo verificar que en el campo “campo”::text de la tabla tabla::text (variable) existe un valor con lo que está contenido en “vrecord”, de lo contrario retorno NULL y el trigger no se ejecuta (la operación de inserción no ocurre).

Sería algo como un “constraint” pero sin usar reglas de constraint, ¿entendido?.

IF TG_OP = 'INSERT' THEN

EXECUTE format('SELECT '||quote_ident(campo)||' FROM '||quote_ident(tabla)||' WHERE '||quote_ident(campo)||' =$1') USING vrecord INTO id;

En este caso, he ejecutado un SELECT (SELECT “campo”::text FROM “tabla”::text WHERE campo::text = vrecord), claro que haciendo las conversiones y los reemplazos respectivos.

El valor de esa ejecución lo agrego a la variable id.

Si adicionalmente se desea averiguar si esa consulta anterior retornó filas, colocamos seguidamente al EXECUTE:

GET DIAGNOSTICS rc = ROW_COUNT;

Si “rc” es igual a cero, entonces no existe el valor “vrecord” en el campo “campo” de la tabla “tabla”, caso contrario, se retorna NEW.

 IF rc = 0 THEN
    RAISE EXCEPTION 'no existe el valor % en el campo % de la tabla %', vrecord, campo, tabla;
    RETURN NULL;
END IF;
RETURN NEW;

Y listo!, definimos el cierre y que esta es una función VOLATILE:

END;
LANGUAGE plpgsql VOLATILE

Y ya podemos usarla.

Usando la función dinámica

Para usar la función dinámica, simplemente creamos un trigger en cada tabla que necesite convocarla, pasando como parámetros de la función trigger la tabla referencia y el campo que debe evaluar, ejemplo:

CREATE TRIGGER trg_insert_detalle_reportes
BEFORE INSERT
ON reportes
FOR EACH ROW
EXECUTE PROCEDURE permitir_insert('reportes', 'id_reporte');

Se insertará un detalle de reporte, solamente si el valor de “id_reporte” aparece en la tabla “reportes”.

Conclusiones

Sé que parece muy “rebuscado” un ejemplo que bien podría salir con una clave foránea, pero sirve para el hecho de demostrar la posibilidad de obtener e iterar sobre el objeto NEW, consultar metadata al “information_schema” o realizar cualquier operación de manera dinámica, pasando parámetros y consultando las variables mágicas TG_* de postgreSQL.

¿Se les ocurre alguna idea para estos triggers dinámicos? …

Acerca de phenobarbital

http://about.me/phenobarbital

Publicado el 6 septiembre 2014 en postgreSQL, Programacion, trucos de la abuela y etiquetado en , , , . Guarda el enlace permanente. Deja un comentario.

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s

A %d blogueros les gusta esto: