sql >> Base de Datos >  >> RDS >> PostgreSQL

Desencadenadores de PostgreSQL y conceptos básicos de funciones almacenadas

Nota de Variousnines:este blog se publica póstumamente debido a la muerte de Berend Tober el 16 de julio de 2018. Honramos sus contribuciones a la comunidad de PostgreSQL y deseamos paz para nuestro amigo y escritor invitado.

En un artículo anterior, discutimos el pseudotipo serial de PostgreSQL, que es útil para completar valores de clave sintética con números enteros incrementales. Vimos que el empleo de la palabra clave de tipo de datos en serie en una declaración de lenguaje de definición de datos de tabla (DDL) se implementa como una declaración de columna de tipo entero que se completa, al insertar una base de datos, con un valor predeterminado derivado de una llamada de función simple. Este comportamiento automatizado de invocar código funcional como parte de la respuesta integral a la actividad del lenguaje de manipulación de datos (DML) es una característica poderosa de los sofisticados sistemas de administración de bases de datos relacionales (RDBMS) como PostgreSQL. En este artículo, profundizamos en otro aspecto más capaz de invocar automáticamente código personalizado, a saber, el uso de activadores y funciones almacenadas.Introducción

Casos de uso para disparadores y funciones almacenadas

Hablemos de por qué es posible que desee invertir en comprender los disparadores y las funciones almacenadas. Al crear código DML en la propia base de datos, puede evitar la implementación duplicada de código relacionado con datos en múltiples aplicaciones separadas que pueden crearse para interactuar con la base de datos. Esto garantiza la ejecución coherente del código DML para la validación de datos, la limpieza de datos u otras funciones, como la auditoría de datos (es decir, el registro de cambios) o el mantenimiento de una tabla de resumen independientemente de cualquier aplicación de llamada. Otro uso común de los disparadores y las funciones almacenadas es hacer que las vistas se puedan escribir, es decir, habilitar inserciones y/o actualizaciones en vistas complejas o proteger ciertos datos de columnas de modificaciones no autorizadas. Además, los datos procesados ​​en el servidor en lugar de en el código de la aplicación no cruzan la red, por lo que existe un menor riesgo de que los datos estén expuestos a espionaje, así como una reducción en la congestión de la red. Además, en PostgreSQL, las funciones almacenadas se pueden configurar para ejecutar código con un nivel de privilegio más alto que el del usuario de la sesión, lo que admite algunas capacidades poderosas. Haremos algunos ejemplos más adelante.

El caso contra los disparadores y las funciones almacenadas

Una revisión de los comentarios en la lista de correo general de PostgreSQL reveló algunas opiniones desfavorables con respecto al uso de activadores y funciones almacenadas que menciono aquí para completar y alentarlo a usted y a su equipo a sopesar los pros y los contras de su implementación.

Entre las objeciones estaba, por ejemplo, la percepción de que las funciones almacenadas no son fáciles de mantener, por lo que se requiere una persona experimentada con habilidades y conocimientos sofisticados en la administración de bases de datos para administrarlas. Algunos profesionales de software han informado que los controles de cambios corporativos en los sistemas de bases de datos suelen ser más estrictos que en el código de la aplicación, por lo que si se implementan reglas comerciales u otra lógica dentro de la base de datos, realizar cambios a medida que evolucionan los requisitos es prohibitivamente engorroso. Otro punto de vista considera los desencadenantes como un efecto secundario inesperado de alguna otra acción y, como tal, pueden ser oscuros, fáciles de pasar por alto, difíciles de depurar y frustrantes de mantener, por lo que, por lo general, deberían ser la última opción, no la primera.

Estas objeciones pueden tener algo de mérito, pero si lo piensa bien, los datos son un activo valioso y, por lo tanto, probablemente desee una persona o un equipo capacitado y experimentado responsable del RDBMS en una organización corporativa o gubernamental de todos modos, y de manera similar, cambie Los tableros de control son un componente comprobado del mantenimiento sostenible de un sistema de información de registro, y el efecto secundario de una persona es igualmente una poderosa conveniencia para otra, que es el punto de vista adoptado para el resto de este artículo.

Declaración de un disparador

Empecemos a aprender las tuercas y los tornillos. Hay muchas opciones disponibles en la sintaxis DDL general para declarar un disparador, y llevaría mucho tiempo tratar todas las permutaciones posibles, por lo que, en aras de la brevedad, hablaremos solo de un subconjunto mínimamente necesario de ellas en ejemplos que siga usando esta sintaxis abreviada:

CREATE TRIGGER name { BEFORE | AFTER | INSTEAD OF } { event [ OR ... ] }
    ON table_name
    FOR EACH ROW EXECUTE PROCEDURE function_name()

where event can be one of:

    INSERT
    UPDATE [ OF column_name [, ... ] ]
    DELETE
    TRUNCATE

Los elementos configurables requeridos además de un nombre son los cuando , el por qué , el dónde y el qué , es decir, el tiempo para que se invoque el código de activación en relación con la acción de activación (cuándo), el tipo específico de instrucción DML de activación (por qué), la tabla o tablas en las que se ha actuado (dónde) y el código de función almacenado para ejecutar (qué).

Declaración de una función

La declaración de activación anterior requiere la especificación de un nombre de función, por lo que, técnicamente, la declaración de activación DDL no se puede ejecutar hasta que la función de activación se haya definido previamente. La sintaxis general de DDL para una declaración de función también tiene muchas opciones, por lo que para facilitar la gestión usaremos esta sintaxis mínimamente suficiente para nuestros propósitos aquí:

CREATE [ OR REPLACE ] FUNCTION
    name () RETURNS TRIGGER
  { LANGUAGE lang_name
    | SECURITY DEFINER
    | SET configuration_parameter { TO value | = value | FROM CURRENT }
    | AS 'definition'
  }...

Una función de disparo no toma parámetros y el tipo de valor devuelto debe ser TRIGGER. Hablaremos sobre los modificadores opcionales a medida que los encontremos en los ejemplos a continuación.

Un esquema de nombres para activadores y funciones

Se ha atribuido al respetado científico informático Phil Karlton haber declarado (en forma parafraseada aquí) que nombrar cosas es uno de los mayores desafíos para los equipos de software. Voy a presentar aquí un disparador fácil de usar y una convención de nomenclatura de funciones almacenadas que me ha servido mucho y lo animo a considerar adoptarlo para sus propios proyectos RDBMS. El esquema de nombres en los ejemplos de este artículo sigue un patrón de uso del nombre de la tabla asociada con el sufijo de una abreviatura que indica el activador declarado cuando y por qué atributos:la primera letra del sufijo será una "b", "a" o "i" (para "antes", "después" o "en lugar de"), la siguiente será una o más de una "i" , "u", "d" o "t" (para "insertar", "actualizar", "eliminar" o "truncar"), y la última letra es solo una "t" para activar. (Utilizo una convención de nomenclatura similar para las reglas y, en ese caso, la última letra es "r"). Entonces, por ejemplo, las diversas combinaciones de atributos de declaración de activación mínima para una tabla llamada "my_table" serían:

|-------------+-------------+-----------+---------------+-----------------|
|  TABLE NAME |  WHEN       |  WHY      |  TRIGGER NAME |  FUNCTION NAME  |
|-------------+-------------+-----------+---------------+-----------------|
|  my_table   |  BEFORE     |  INSERT   |  my_table_bit |  my_table_bit   |
|  my_table   |  BEFORE     |  UPDATE   |  my_table_but |  my_table_but   |
|  my_table   |  BEFORE     |  DELETE   |  my_table_bdt |  my_table_bdt   |
|  my_table   |  BEFORE     |  TRUNCATE |  my_table_btt |  my_table_btt   |
|  my_table   |  AFTER      |  INSERT   |  my_table_ait |  my_table_ait   |
|  my_table   |  AFTER      |  UPDATE   |  my_table_aut |  my_table_aut   |
|  my_table   |  AFTER      |  DELETE   |  my_table_adt |  my_table_adt   |
|  my_table   |  AFTER      |  TRUNCATE |  my_table_att |  my_table_att   |
|  my_table   |  INSTEAD OF |  INSERT   |  my_table_iit |  my_table_iit   |
|  my_table   |  INSTEAD OF |  UPDATE   |  my_table_iut |  my_table_iut   |
|  my_table   |  INSTEAD OF |  DELETE   |  my_table_idt |  my_table_idt   |
|  my_table   |  INSTEAD OF |  TRUNCATE |  my_table_itt |  my_table_itt   |
|-------------+-------------+-----------+---------------+-----------------|

Se puede usar exactamente el mismo nombre tanto para el disparador como para la función almacenada asociada, lo cual es completamente permisible en PostgreSQL porque el RDBMS realiza un seguimiento de los disparadores y las funciones almacenadas por separado según los propósitos respectivos, y el contexto en el que se usa el nombre del elemento hace borrar a qué elemento se refiere el nombre.

Entonces, por ejemplo, una declaración de activación correspondiente al escenario de la primera fila de la tabla anterior se implementaría como

CREATE TRIGGER my_table_bit 
    BEFORE INSERT
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_bit();

En el caso de que se declare un disparador con múltiples por qué atributos, simplemente expanda el sufijo apropiadamente, por ejemplo, para un insertar o actualizar disparador, lo anterior se convertiría

CREATE TRIGGER my_table_biut 
    BEFORE INSERT OR UPDATE
    ON my_table
    FOR EACH ROW EXECUTE PROCEDURE my_table_biut();

¡Muéstrame algo de código ya!

Hagámoslo realidad. Comenzaremos con un ejemplo simple y luego lo ampliaremos para ilustrar más funciones. Las instrucciones DDL desencadenantes requieren una función preexistente, como se mencionó, y también una tabla sobre la cual actuar, por lo que primero necesitamos una tabla en la que trabajar. Por ejemplo, digamos que necesitamos almacenar datos básicos de identidad de la cuenta

CREATE TABLE person (
    login_name varchar(9) not null primary key,
    display_name text
);

Cierta aplicación de la integridad de los datos se puede manejar simplemente con la columna DDL adecuada, como en este caso el requisito de que el nombre_de_inicio de sesión exista y no tenga más de nueve caracteres. Los intentos de insertar un valor NULL o un valor demasiado largo de login_name fallan y reportan mensajes de error significativos:

INSERT INTO person VALUES (NULL, 'Felonious Erroneous');
ERROR:  null value in column "login_name" violates not-null constraint
DETAIL:  Failing row contains (null, Felonious Erroneous).

INSERT INTO person VALUES ('atoolongusername', 'Felonious Erroneous');
ERROR:  value too long for type character varying(9)

Se pueden manejar otros cumplimientos con restricciones de verificación, como requerir una longitud mínima y rechazar ciertos caracteres:

ALTER TABLE person 
    ADD CONSTRAINT PERSON_LOGIN_NAME_NON_NULL 
    CHECK (LENGTH(login_name) > 0);

ALTER TABLE person 
    ADD CONSTRAINT person_login_name_no_space 
    CHECK (POSITION(' ' IN login_name) = 0);

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  new row for relation "person" violates check constraint "person_login_name_non_null"
DETAIL:  Failing row contains (, Felonious Erroneous).

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  new row for relation "person" violates check constraint "person_login_name_no_space"
DETAIL:  Failing row contains (space man, Major Tom).

pero tenga en cuenta que el mensaje de error no es tan informativo como antes, ya que transmite solo lo que está codificado en el nombre del activador en lugar de un mensaje de texto explicativo significativo. Al implementar la lógica de verificación en una función almacenada, puede usar una excepción para emitir un mensaje de texto más útil. Además, las expresiones de restricción de verificación no pueden contener subconsultas ni hacer referencia a variables que no sean columnas de la fila actual ni otras tablas de la base de datos.

Así que dejemos las restricciones de verificación

ALTER TABLE PERSON DROP CONSTRAINT person_login_name_no_space;
ALTER TABLE PERSON DROP CONSTRAINT person_login_name_non_null;

y continuar con disparadores y funciones almacenadas.

Muéstrame más código

Tenemos una mesa. Pasando a la función DDL, definimos una función de cuerpo vacío, que podemos completar más tarde con un código específico:

CREATE OR REPLACE FUNCTION person_bit() 
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    SET search_path = public
    AS '
    BEGIN
    END;
    ';

Esto nos permite llegar finalmente al disparador DDL conectando la tabla y la función para que podamos hacer algunos ejemplos:

CREATE TRIGGER person_bit 
    BEFORE INSERT ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

PostgreSQL permite que las funciones almacenadas se escriban en una variedad de lenguajes diferentes. En este caso y en los siguientes ejemplos, estamos componiendo funciones en el lenguaje PL/pgSQL que está diseñado específicamente para PostgreSQL y admite el uso de todos los tipos de datos, operadores y funciones de PostgreSQL RDBMS. La opción SET SCHEMA establece la ruta de búsqueda del esquema que se utilizará durante la ejecución de la función. Establecer la ruta de búsqueda para cada función es una buena práctica, ya que evita tener que prefijar los objetos de la base de datos con un nombre de esquema y protege contra ciertas vulnerabilidades relacionadas con la ruta de búsqueda.

EJEMPLO 0 - Validación de datos

Como primer ejemplo, implementemos las comprobaciones anteriores, pero con mensajes más amigables para los humanos.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;
    RETURN NEW;
    END;
    $$;

El calificador “NUEVO” es una referencia a la fila de datos que se va a insertar. Es una de varias variables especiales disponibles dentro de una función de disparo. Presentaremos algunos otros a continuación. Tenga en cuenta también que PostgreSQL permite la sustitución de las comillas simples que delimitan el cuerpo de la función con otros delimitadores, en este caso siguiendo una convención común de usar signos de dólar dobles como delimitador, ya que el cuerpo de la función en sí incluye caracteres de comillas simples. Las funciones de activación deben salir devolviendo la NUEVA fila que se va a insertar o NULL para cancelar la acción en silencio.

Los mismos intentos de inserción fallan como se esperaba, pero ahora con mensajes amigables:

INSERT INTO person VALUES ('', 'Felonious Erroneous');
ERROR:  Login name must not be empty.

INSERT INTO person VALUES ('space man', 'Major Tom');
ERROR:  Login name must not include white space.

EJEMPLO 1 - Registro de auditoría

Con las funciones almacenadas, tenemos una amplia libertad en cuanto a lo que hace el código invocado, incluida la referencia a otras tablas (lo que no es posible con las restricciones de verificación). Como un ejemplo más complejo, veremos la implementación de una tabla de auditoría, es decir, mantener un registro, en una tabla separada, de inserciones, actualizaciones y eliminaciones en una tabla principal. La tabla de auditoría generalmente contiene los mismos atributos que la tabla principal, que se utilizan para registrar los valores modificados, además de atributos adicionales para registrar la operación ejecutada para realizar el cambio, así como una marca de tiempo de la transacción y un registro del usuario que realiza el cambio. cambiar:

CREATE TABLE person_audit (
    login_name varchar(9) not null,
    display_name text,
    operation varchar,
    effective_at timestamp not null default now(),
    userid name not null default session_user
);

En este caso, implementar la auditoría es muy fácil, simplemente modificamos la función de activación existente para incluir DML para efectuar la inserción de la tabla de auditoría, y luego redefinimos la activación para que se active tanto en las actualizaciones como en las inserciones. Tenga en cuenta que hemos optado por no cambiar el sufijo del nombre de la función de activación a "biut", pero si la funcionalidad de auditoría hubiera sido un requisito conocido en el momento del diseño inicial, ese sería el nombre utilizado:

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- New code to record audits

    INSERT INTO person_audit (login_name, display_name, operation) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP);

    RETURN NEW;
    END;
    $$;


DROP TRIGGER person_bit ON person;

CREATE TRIGGER person_biut 
    BEFORE INSERT OR UPDATE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bit();

Tenga en cuenta que hemos introducido otra variable especial "TG_OP" que el sistema establece para identificar la operación DML que disparó el activador como "INSERTAR", "ACTUALIZAR", "ELIMINAR" o "TRUNCAR", respectivamente.

Necesitamos manejar las eliminaciones por separado de las inserciones y actualizaciones, ya que las pruebas de validación de atributos son superfluas y porque el valor especial NUEVO no se define al ingresar a un antes de eliminar activar la función y así definir la correspondiente función almacenada y activar:

CREATE OR REPLACE FUNCTION person_bdt()
    RETURNS TRIGGER
    SET SCHEMA 'public'
    LANGUAGE plpgsql
    AS $$
    BEGIN

    -- Record deletion in audit table

    INSERT INTO person_audit (login_name, display_name, operation) 
      VALUES (OLD.login_name, OLD.display_name, TG_OP);

    RETURN OLD;
    END;
    $$;
        
CREATE TRIGGER person_bdt 
    BEFORE DELETE ON person
    FOR EACH ROW EXECUTE PROCEDURE person_bdt();

Tenga en cuenta el uso del valor especial OLD como referencia a la fila que está a punto de eliminarse, es decir, la fila tal como existe antes ocurre la eliminación.

Hacemos un par de inserciones para probar la funcionalidad y confirmar que la tabla de auditoría incluye un registro de las inserciones:

INSERT INTO person VALUES ('dfunny', 'Doug Funny');
INSERT INTO person VALUES ('pmayo', 'Patti Mayonnaise');

SELECT * FROM person;
 login_name |   display_name   
------------+------------------
 dfunny     | Doug Funny
 pmayo      | Patti Mayonnaise
(2 rows)

SELECT * FROM person_audit;
 login_name |   display_name   | operation |        effective_at        |  userid  
------------+------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny       | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise | INSERT    | 2018-05-26 18:48:07.698623 | postgres
(2 rows)

Luego, actualizamos una fila y confirmamos que la tabla de auditoría incluye un registro del cambio agregando un segundo nombre a uno de los nombres para mostrar del registro de datos:

UPDATE person SET display_name = 'Doug Yancey Funny' WHERE login_name = 'dfunny';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 pmayo      | Patti Mayonnaise
 dfunny     | Doug Yancey Funny
(2 rows)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-26 18:48:07.6903   | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-26 18:48:07.698623 | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-26 18:48:07.707284 | postgres
(3 rows)

Y, por último, ejercitamos la funcionalidad de eliminación y confirmamos que la tabla de auditoría también incluye ese registro:

DELETE FROM person WHERE login_name = 'pmayo';

SELECT * FROM person;
 login_name |   display_name    
------------+-------------------
 dfunny     | Doug Yancey Funny
(1 row)

SELECT * FROM person_audit ORDER BY effective_at;
 login_name |   display_name    | operation |        effective_at        |  userid  
------------+-------------------+-----------+----------------------------+----------
 dfunny     | Doug Funny        | INSERT    | 2018-05-27 08:13:22.747226 | postgres
 pmayo      | Patti Mayonnaise  | INSERT    | 2018-05-27 08:13:22.74839  | postgres
 dfunny     | Doug Yancey Funny | UPDATE    | 2018-05-27 08:13:22.749495 | postgres
 pmayo      | Patti Mayonnaise  | DELETE    | 2018-05-27 08:13:22.753425 | postgres
(4 rows)

EJEMPLO 2 - Valores derivados

Vayamos un paso más allá e imaginemos que queremos almacenar algún documento de texto de forma libre dentro de cada fila, por ejemplo, un currículum con formato de texto sin formato, un documento de una conferencia o un resumen de un personaje de entretenimiento, y queremos admitir el uso de la potente búsqueda de texto completo. capacidades de PostgreSQL en estos documentos de texto de formato libre.

Primero agregamos dos atributos para admitir el almacenamiento del documento y de un vector de búsqueda de texto asociado a la tabla principal. Dado que el vector de búsqueda de texto se deriva fila por fila, no tiene sentido almacenarlo en la tabla de auditoría, aunque agreguemos la columna de almacenamiento de documentos a la tabla de auditoría asociada:

ALTER TABLE person ADD COLUMN abstract TEXT;
ALTER TABLE person ADD COLUMN ts_abstract TSVECTOR;

ALTER TABLE person_audit ADD COLUMN abstract TEXT;

Luego modificamos la función de activación para procesar estos nuevos atributos. La columna de texto sin formato se maneja de la misma manera que otros datos ingresados ​​por el usuario, pero el vector de búsqueda de texto es un valor derivado y, por lo tanto, se maneja mediante una llamada de función que reduce el texto del documento a un tipo de datos tsvector para una búsqueda eficiente.

CREATE OR REPLACE FUNCTION person_bit()
    RETURNS TRIGGER
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    BEGIN
    IF LENGTH(NEW.login_name) = 0 THEN
        RAISE EXCEPTION 'Login name must not be empty.';
    END IF;

    IF POSITION(' ' IN NEW.login_name) > 0 THEN
        RAISE EXCEPTION 'Login name must not include white space.';
    END IF;

    -- Modified audit code to include text abstract

    INSERT INTO person_audit (login_name, display_name, operation, abstract) 
        VALUES (NEW.login_name, NEW.display_name, TG_OP, NEW.abstract);

    -- New code to reduce text to text-search vector

    SELECT to_tsvector(NEW.abstract) INTO NEW.ts_abstract;

    RETURN NEW;
    END;
    $$;

Como prueba, actualizamos una fila existente con texto detallado de Wikipedia:

UPDATE person SET abstract = 'Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd.' WHERE login_name = 'dfunny';

y luego confirme que el procesamiento del vector de búsqueda de texto fue exitoso:

SELECT login_name, ts_abstract  FROM person;
 login_name |                                                                                                                ts_abstract                                                                                                                
------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 dfunny     | '11':11 '12':13 'an':5 'and':9 'as':4 'boy':16 'crowd':24 'depicted':3 'doug':1 'fit':20 'gullible':10 'in':21 'insecure':8 'introverted':6 'is':2 'later':12 'old':15 'quiet':7 'the':23 'to':19 'wants':18 'who':17 'with':22 'year':14
(1 row)

EJEMPLO 3:disparadores y vistas

El vector de búsqueda de texto derivado del ejemplo anterior no está destinado al consumo humano, es decir, no lo ingresa el usuario y nunca esperamos presentar el valor a un usuario final. Si un usuario intenta insertar un valor para la columna ts_abstract, todo lo proporcionado se descartará y se reemplazará con el valor derivado internamente de la función de activación, por lo que tenemos protección contra el envenenamiento del corpus de búsqueda. Para ocultar la columna por completo, podemos definir una vista abreviada que no incluya ese atributo, pero aun así obtenemos el beneficio de activar la actividad en la tabla subyacente:

CREATE VIEW abridged_person AS SELECT login_name, display_name, abstract FROM person;

Para una vista simple, PostgreSQL automáticamente hace que se pueda escribir para que no tengamos que hacer nada más para insertar o actualizar datos con éxito. Cuando el DML entra en vigor en la tabla subyacente, los disparadores se activan como si la declaración se aplicara directamente a la tabla, por lo que aún obtenemos la compatibilidad con la búsqueda de texto ejecutada en segundo plano, llenando la columna del vector de búsqueda de la tabla de personas y agregando el cambiar información a la tabla de auditoría:

INSERT INTO abridged_person VALUES ('skeeter', 'Mosquito Valentine', 'Skeeter is Doug''s best friend. He is famous in both series for the honking sounds he frequently makes.');


SELECT login_name, ts_abstract FROM person WHERE login_name = 'skeeter';
 login_name |                                                                                   ts_abstract                                                                                    
------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 skeeter    | 'best':5 'both':11 'doug':3 'famous':9 'for':13 'frequently':18 'friend':6 'he':7,17 'honking':15 'in':10 'is':2,8 'makes':19 's':4 'series':12 'skeeter':1 'sounds':16 'the':14
(1 row)


SELECT login_name, display_name, operation, userid FROM person_audit ORDER BY effective_at;
 login_name |    display_name    | operation |  userid  
------------+--------------------+-----------+----------
 dfunny     | Doug Funny         | INSERT    | postgres
 pmayo      | Patti Mayonnaise   | INSERT    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 pmayo      | Patti Mayonnaise   | DELETE    | postgres
 dfunny     | Doug Yancey Funny  | UPDATE    | postgres
 skeeter    | Mosquito Valentine | INSERT    | postgres
(6 rows)

Para vistas más complicadas que no cumplen los requisitos para permitir la escritura automática, el sistema de reglas o en lugar de los disparadores pueden hacer el trabajo para admitir escrituras y eliminaciones.

EJEMPLO 4 - Valores de resumen

Embellecemos aún más y tratemos el escenario donde hay algún tipo de tabla de transacciones. Puede ser un registro de horas trabajadas, aumentos de inventario y reducciones de existencias de almacén o minoristas, o tal vez un registro de verificación con débitos y créditos para cada persona:

CREATE TABLE transaction (
    login_name character varying(9) NOT NULL,
    post_date date,
    description character varying,
    debit money,
    credit money,
    FOREIGN KEY (login_name) REFERENCES person (login_name)
);

Y digamos que si bien es importante conservar el historial de transacciones, las reglas comerciales implican usar el saldo neto en el procesamiento de la solicitud en lugar de los detalles de la transacción. Para evitar tener que recalcular con frecuencia el saldo sumando todas las transacciones cada vez que se necesita el saldo, podemos desnormalizar y mantener un valor de saldo actual allí mismo en la tabla de personas agregando una nueva columna y usando un activador y una función almacenada para mantener el saldo neto a medida que se insertan las transacciones:

ALTER TABLE person ADD COLUMN balance MONEY DEFAULT 0;

CREATE FUNCTION transaction_bit() RETURNS trigger
    LANGUAGE plpgsql
    SET SCHEMA 'public'
    AS $$
    DECLARE
    newbalance money;
    BEGIN

    -- Update person account balance

    UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name
                RETURNING balance INTO newbalance;

    -- Data validation

    IF COALESCE(NEW.debit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Debit value must be non-negative';
    END IF;

    IF COALESCE(NEW.credit, 0::money) < 0::money THEN
        RAISE EXCEPTION 'Credit value must be non-negative';
    END IF;

    IF newbalance < 0::money THEN
        RAISE EXCEPTION 'Insufficient funds: %', NEW;
    END IF;

    RETURN NEW;
    END;
    $$;



CREATE TRIGGER transaction_bit 
      BEFORE INSERT ON transaction 
      FOR EACH ROW EXECUTE PROCEDURE transaction_bit();

Puede parecer extraño hacer la actualización primero en la función almacenada antes de validar la no negatividad de los valores de débito, crédito y saldo, pero en términos de validación de datos, el orden no importa porque el cuerpo de una función de activación se ejecuta como un transacción de la base de datos, por lo que si esas comprobaciones de validación fallan, la transacción completa se revierte cuando se genera la excepción. La ventaja de hacer la actualización primero es que la actualización bloquea la fila afectada durante la transacción y, por lo tanto, cualquier otra sesión que intente actualizar la misma fila se bloquea hasta que se complete la transacción actual. La prueba de validación adicional asegura que el saldo resultante no sea negativo, y el mensaje de información de excepción puede incluir una variable, que en este caso devolverá la fila de transacción de inserción infractora para su depuración.

Para demostrar que realmente funciona, aquí hay algunas entradas de muestra y un cheque que muestra el saldo actualizado en cada paso:

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name | balance 
------------+---------
 dfunny     |   $0.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-11', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)
INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$2780.52', NULL);
ERROR:  Insufficient funds: (dfunny,2018-01-17,"FOR:BGE PAYMENT ACH Withdrawal",,"$2,780.52")

Tenga en cuenta cómo la transacción anterior falla con fondos insuficientes, es decir, produciría un saldo negativo y se revertiría con éxito. También tenga en cuenta que devolvimos la fila completa con la NUEVA variable especial como detalle adicional en el mensaje de error para la depuración.

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $2,000.00
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-17', 'FOR:BGE PAYMENT ACH Withdrawal', '$278.52', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,721.48
(1 row)

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal', '$35.29', NULL);

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

EJEMPLO 5 - Disparadores y Vistas Redux

Sin embargo, hay un problema con la implementación anterior, y es que nada evita que un usuario malintencionado imprima dinero:

BEGIN;
UPDATE person SET balance = '1000000000.00';

SELECT login_name, balance FROM person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Hemos revertido el robo anterior por ahora y mostraremos una forma de crear protección contra el uso de un activador en una vista para evitar actualizaciones en el valor del saldo.

Primero aumentamos la vista abreviada anterior para exponer la columna de saldo:

CREATE OR REPLACE VIEW abridged_person AS
  SELECT login_name, display_name, abstract, balance FROM person;

Obviamente, esto permite el acceso de lectura al balance, pero aún no resuelve el problema porque para vistas simples como esta basadas en una sola tabla, PostgreSQL automáticamente hace que la vista se pueda escribir:

BEGIN;
UPDATE abridged_person SET balance = '1000000000.00';
SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |      balance      
------------+-------------------
 dfunny     | $1,000,000,000.00
(1 row)

ROLLBACK;

Podríamos usar una regla, pero para ilustrar que los disparadores se pueden definir tanto en vistas como en tablas, tomaremos la última ruta y usaremos en lugar de actualizar desencadenar en la vista para bloquear DML no deseado, evitando cambios no transaccionales en el valor del saldo:

CREATE FUNCTION abridged_person_iut() RETURNS TRIGGER
    LANGUAGE plpgsql
    SET search_path TO public
    AS $$
    BEGIN

    -- Disallow non-transactional changes to balance

      NEW.balance = OLD.balance;
    RETURN NEW;
    END;
    $$;

CREATE TRIGGER abridged_person_iut
    INSTEAD OF UPDATE ON abridged_person
    FOR EACH ROW EXECUTE PROCEDURE abridged_person_iut();

The above instead of update trigger and stored procedure discards any attempted updates to the balance value and instead forces use of the value present in the database prior to the triggering update statement:

UPDATE abridged_person SET balance = '1000000000.00';

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $1,686.19
(1 row)

which affords protection against un-auditable changes to the balance value.

Descargue el documento técnico hoy Administración y automatización de PostgreSQL con ClusterControlObtenga información sobre lo que necesita saber para implementar, monitorear, administrar y escalar PostgreSQLDescargar el documento técnico

EXAMPLE 6 - Elevated Privileges

So far all the example code above has been executed at the database owner level by the postgres login role, so any of our anti-tampering efforts could be obviated… that’s just a fact of the database owner super-user privileges.

Our final example illustrates how triggers and stored functions can be used to allow the execution of code by a non-privileged user at a higher privilege than the logged in session user normally has by employing the SECURITY DEFINER attribute associated with stored functions.

First, we define a non-privileged login role, eve and confirm that upon instantiation there are no privileges:

CREATE USER eve;
\dp
                                  Access privileges
 Schema |      Name       | Type  | Access privileges | Column privileges | Policies 
--------+-----------------+-------+-------------------+-------------------+----------
 public | abridged_person | view  |                   |                   | 
 public | person          | table |                   |                   | 
 public | person_audit    | table |                   |                   | 
 public | transaction     | table |                   |                   | 
(4 rows)

We grant read, update, and create privileges on the abridged person view and read and create to the transaction table:

GRANT SELECT,INSERT, UPDATE ON abridged_person TO eve;
GRANT SELECT,INSERT ON transaction TO eve;
\dp
                                      Access privileges
 Schema |      Name       | Type  |     Access privileges     | Column privileges | Policies 
--------+-----------------+-------+---------------------------+-------------------+----------
 public | abridged_person | view  | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=arw/postgres          |                   | 
 public | person          | table |                           |                   | 
 public | person_audit    | table |                           |                   | 
 public | transaction     | table | postgres=arwdDxt/postgres+|                   | 
        |                 |       | eve=ar/postgres           |                   | 
(4 rows)

By way of confirmation we see that eve is denied access to the person and person_audit tables:

SET SESSION AUTHORIZATION eve;

SELECT * FROM person;
ERROR:  permission denied for relation person

SELECT * from person_audit;
ERROR:  permission denied for relation person_audit

and that she does have appropriate read access to the abridged_person and transaction tables:

SELECT * FROM abridged_person;
 login_name |    display_name    |                                                            abstract                                                             |  balance  
------------+--------------------+---------------------------------------------------------------------------------------------------------------------------------+-----------
 skeeter    | Mosquito Valentine | Skeeter is Doug's best friend. He is famous in both series for the honking sounds he frequently makes.                          |     $0.00
 dfunny     | Doug Yancey Funny  | Doug is depicted as an introverted, quiet, insecure and gullible 11 (later 12) year old boy who wants to fit in with the crowd. | $1,686.19
(2 rows)

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
(3 rows)

However, even though she has write privilege on the transaction table, a transaction insert attempt fails due to lack of privilege on the person mesa.

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');
ERROR:  permission denied for relation person
CONTEXT:  SQL statement "UPDATE person 
        SET balance = 
            balance + 
            COALESCE(NEW.debit, 0::money) - 
            COALESCE(NEW.credit, 0::money) 
        WHERE login_name = NEW.login_name"
PL/pgSQL function transaction_bit() line 6 at SQL statement

The error message context shows this hold up occurs when inside the trigger function DML to update the balance is invoked. The way around this need to deny Eve direct write access to the person table but still effect updates to the person balance in a controlled manner is to add the SECURITY DEFINER attribute to the stored function:

RESET SESSION AUTHORIZATION;
ALTER FUNCTION transaction_bit() SECURITY DEFINER;

SET SESSION AUTHORIZATION eve;

INSERT INTO transaction (login_name, post_date, description, credit, debit) VALUES ('dfunny', '2018-01-23', 'ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit', NULL, '$2,000.00');

SELECT * FROM transaction;
 login_name | post_date  |                         description                          |   debit   | credit  
------------+------------+--------------------------------------------------------------+-----------+---------
 dfunny     | 2018-01-11 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
 dfunny     | 2018-01-17 | FOR:BGE PAYMENT ACH Withdrawal                               |           | $278.52
 dfunny     | 2018-01-23 | FOR: ANNE ARUNDEL ONLINE PMT ACH Withdrawal                  |           |  $35.29
 dfunny     | 2018-01-23 | ACH CREDIT FROM: FINANCE AND ACCO ALLOTMENT : Direct Deposit | $2,000.00 |        
(4 rows)

SELECT login_name, balance FROM abridged_person WHERE login_name = 'dfunny';
 login_name |  balance  
------------+-----------
 dfunny     | $3,686.19
(1 row)

Now the transaction insert succeeds because the stored function is executed with privilege level of its definer, i.e., the postgres user, which does have the appropriate write privilege on the person table.

Conclusión

As lengthy as this article is, there’s still a lot more to say about triggers and stored functions. What we covered here is a basic introduction with a consideration of pros and cons of triggers and stored functions. We illustrated six use-case examples showing data validation, change logging, deriving values from inserted data, data hiding with simple updatable views, maintaining summary data in separate tables, and allowing safe invocation of code at elevated privilege. Look for a future article on using triggers and stored functions to prevent missing values in sequentially-incrementing (serial) columns.