miércoles, 17 de abril de 2013

Creación de constraint Unique Key con valores específicos que se puedan repetir en PostgreSQL. Constraint personalizado.


Si usted necesita crear una validación en la base de datos postgres que verifique los valores de una columna sean únicos excepto uno o más valores, a continuación se presenta como hacerlo mediante función trigger con su respectivo ejemplo.

Caso ejemplo

Se tiene una tabla que almacena los datos de los equipos informáticos llamados activos, los cuales se identifican mediante un serial asignado por el fabricante. La tabla se crea con la siguiente estructura en el esquema activo:

CREATE TABLE activo.activo
(
   co_activo numeric(9,0),
   tx_serial character varying(30) NOT NULL DEFAULT 'S/S',
   tx_descripcion character varying(300) NOT NULL
);

Obsérvese que la columna tx_serial tiene como valor por defecto la cadena 'S/S' la cual representa Sin Serial.

Considerando que los activos para el usuario serán identificados por el serial y no por el código de identificación asignado por la base de datos (co_activo), y que existen activos que de fábrica no tienen serial, se requiere permitir que el único valor que se permite repetir en la columna es 'S/S'. Cualquier otro valor diferente al indicado, se tomará como el valor serial que identifica unívocamente el activo. Justamente es allí donde se presenta el problema, dado que, si creamos una clave de chequeo único (unique key) se asegura que ningún valor se repita, pero no se podrán identificar aquellos activos que no poseen serial con el valor S/S. Por ende, se procede a crear la clave única con excepción utilizando una función con disparador (function trigger).

Creación de la función

Para crear la función se deben considerar los siguientes aspectos:
1.           Primero se debe crear la función y luego el disparador.
2.           La función devolverá un valor de tipo trigger.
3.           La función será llamada para eventos Insert/Update.
4.           Siempre será devuelto el registro NEW que contiene los valores que se desean insertar o actualizar en al fila.

A continuación el código de la función encontrarSerial:

CREATE OR REPLACE FUNCTION activo.encontrarSerial()
  RETURNS "trigger" AS
$BODY$
DECLARE
        registro activo.activo%ROWTYPE;
BEGIN
        IF (TG_OP = 'INSERT') THEN
                SELECT * INTO registro FROM activo.activo A
                WHERE A.tx_serial <>'S/S' and A.tx_serial = NEW.tx_serial LIMIT 1;
                IF FOUND THEN
                 RAISE EXCEPTION 'El serial % ya se encuentra registrado. Intente con otro valor.',NEW.tx_serial;
                END IF;
        END IF;
        IF (TG_OP = 'UPDATE') THEN
                SELECT * INTO registro FROM activo.activo A
                WHERE A.tx_serial <>'S/S' and A.tx_serial = NEW.tx_serial and A.co_activo<>NEW.co_activo LIMIT 1;
                IF FOUND THEN
                 RAISE EXCEPTION 'El serial % ya se encuentra registrado. Intente con otro valor.',NEW.tx_serial;
                END IF;
        END IF;
RETURN NEW;
END;
$BODY$
  LANGUAGE plpgsql VOLATILE STRICT;

Explicación:
                     Se declara una variable llamada 'registro' para almacenar la fila encontrada. El tipo de dato de la variable se debe indicar que es del mismo tipo de la fila contrada. Ejm: registro activo.activo%ROWTYPE;
                     Se verifica el tipo de operación capturada por el disparador (trigger) para ejecutar la consulta correspondiente. Ejm: IF (TG_OP = 'INSERT') THEN
                     Se realiza la consulta Select a la tabla activo almacenando el valor encontrado en la variable registro. Ejm: SELECT * INTO registro FROM activo.activo A
                     En la clausula Where de la consulta se busca un valor existente diferente al 'S/S' e igual al nuevo serial. Ejm: WHERE A.tx_serial <>'S/S' and A.tx_serial = NEW.tx_serial LIMIT 1;
                     Se verifica la existencia de registro encontrado. Ejem: IF FOUND THEN
                     Si el valor es encontrado, se dispara un error de excepción y se detiene la operación del insert. Ejm: RAISE EXCEPTION 'El serial % ya se encuentra registrado. Intente con otro valor.',NEW.tx_serial;
                     Si la operación es UPDATE solo cambiará la condición del WHERE agregando que el código de identificación no sea el mismo. Ejm: WHERE A.tx_serial <>'S/S' and A.tx_serial = NEW.tx_serial and A.co_activo<>NEW.co_activo LIMIT 1;
                     Finalmente se retorna el registro NEW para garantizar que el registro devuelto siempre contenga valores. Si se utiliza como valor de retorno la variable registro, se presentaran problemas de ejecución cuando la consulta no encuentra valor repetido y la variable contendrá NULL.

Una vez creada la función encontrarSerial, se procede a crear el disparador:

CREATE TRIGGER triggerEncontrarSerial
  BEFORE INSERT OR UPDATE
  ON activo.activo
  FOR EACH ROW
  EXECUTE PROCEDURE activo.encontrarSerial();

Obsérvese que el disparador se crea para capturar los eventos Insert o Update para cada fila afectada y antes de ejecutar la operación se realizarán las instrucciones indicadas en la función encontrarSerial() especificada en el esquema activo.

Con esta función y disparador se tendrá la seguridad que los valores de la columna no se repetirán excepto el valor indicado 'S/S'.

Conclusión:

Con la creación de la función que retorne trigger la cual contendrá el diseño de la consulta SELECT con las condiciones ajustadas a su necesidad, porque puede agregar tantas excepciones de valores repetidos como los requiera las reglas de negocio de su base de datos y la creación del disparador en la tabla para que se ejecuta la revisión previa a la inserción o modificación del valor de una fila de la tabla activo.