37.1. Descripción general del comportamiento de los disparadores #

Un disparador (trigger) es una especificación de que la base de datos debe ejecutar automáticamente una función particular cada vez que se realice un cierto tipo de operación. Los disparadores se pueden asociar a tablas (particionadas o no), vistas y tablas foráneas.

En tablas y tablas foráneas, los disparadores se pueden definir para que se ejecuten antes o después de cualquier operación INSERT, UPDATE o DELETE, ya sea una vez por fila modificada, o una vez por sentencia SQL. Los disparadores de UPDATE se pueden configurar además para que se activen sólo si se mencionan ciertas columnas en la cláusula SET de la sentencia UPDATE. Los disparadores también se pueden activar para sentencias TRUNCATE. Si ocurre un evento de disparo, la función del disparador se llama en el momento apropiado para manejar el evento.

En las vistas, los disparadores se pueden definir para ejecutarse en lugar de las operaciones INSERT, UPDATE o DELETE. Tales disparadores INSTEAD OF se activan una vez por cada fila que deba modificarse en la vista. Es responsabilidad de la función del disparador realizar las modificaciones necesarias en la(s) tabla(s) base subyacente(s) de la vista y, cuando sea apropiado, devolver la fila modificada tal como aparecerá en la vista. Los disparadores en las vistas también se pueden definir para ejecutarse una vez por sentencia SQL, antes o después de las operaciones INSERT, UPDATE o DELETE. Sin embargo, estos disparadores sólo se activan si también hay un disparador INSTEAD OF en la vista. De lo contrario, cualquier sentencia dirigida a la vista debe reescribirse en una sentencia que afecte a su(s) tabla(s) base subyacente(s), y entonces los disparadores que se activarán serán los asociados a la(s) tabla(s) base.

La función del disparador debe definirse antes de que se pueda crear el disparador en sí. La función del disparador debe declararse como una función que no toma argumentos y devuelve el tipo trigger. (La función del disparador recibe su entrada a través de una estructura TriggerData que se pasa de manera especial, no en forma de argumentos de función ordinarios).

Una vez que se ha creado una función de disparo adecuada, el disparador se establece con CREATE TRIGGER. Se puede usar la misma función de disparo para múltiples disparadores.

PostgreSQL ofrece tanto disparadores por fila (per-row) como disparadores por sentencia (per-statement). Con un disparador por fila, la función del disparador se invoca una vez por cada fila afectada por la sentencia que activó el disparador. En cambio, un disparador por sentencia se invoca una sola vez cuando se ejecuta la sentencia correspondiente, independientemente del número de filas afectadas por esa sentencia. En particular, una sentencia que afecte a cero filas seguirá dando lugar a la ejecución de cualquier disparador por sentencia aplicable. Estos dos tipos de disparadores a veces se llaman disparadores a nivel de fila (row-level) y disparadores a nivel de sentencia (statement-level), respectivamente. Los disparadores en TRUNCATE sólo se pueden definir a nivel de sentencia, no por fila.

Los disparadores también se clasifican según si se activan antes (before), después (after) o en lugar de (instead of) la operación. Se denominan disparadores BEFORE, disparadores AFTER y disparadores INSTEAD OF respectivamente. Los disparadores BEFORE a nivel de sentencia naturalmente se activan antes de que la sentencia comience a hacer algo, mientras que los disparadores AFTER a nivel de sentencia se activan al final de la sentencia. Estos tipos de disparadores se pueden definir en tablas, vistas o tablas foráneas. Los disparadores BEFORE a nivel de fila se activan inmediatamente antes de operar sobre una fila en particular, mientras que los disparadores AFTER a nivel de fila se activan al final de la sentencia (pero antes de cualquier disparador AFTER a nivel de sentencia). Estos tipos de disparadores sólo se pueden definir en tablas y tablas foráneas, no en vistas. Los disparadores INSTEAD OF sólo se pueden definir en vistas, y sólo a nivel de fila; se activan inmediatamente cuando se identifica que cada fila de la vista necesita ser operada.

La ejecución de un disparador AFTER se puede posponer hasta el final de la transacción, en lugar de al final de la sentencia, si se definió como un disparador de restricción (constraint trigger). En todos los casos, un disparador se ejecuta como parte de la misma transacción que la sentencia que lo activó, por lo que si la sentencia o el disparador causan un error, los efectos de ambos se revertirán. Además, el disparador siempre se ejecutará con el rol que puso en cola el evento de disparo, a menos que la función del disparador esté marcada como SECURITY DEFINER, en cuyo caso se ejecutará como el propietario de la función.

Si un INSERT contiene una cláusula ON CONFLICT DO UPDATE, es posible que se ejecuten disparadores BEFORE INSERT a nivel de fila y luego disparadores BEFORE UPDATE en las filas afectadas. Estas interacciones pueden ser complejas si los disparadores no son idempotentes porque los cambios realizados por los disparadores BEFORE INSERT serán vistos por los disparadores BEFORE UPDATE, incluidos los cambios en las columnas EXCLUDED.

Ten en cuenta que los disparadores UPDATE a nivel de sentencia se ejecutan cuando se especifica ON CONFLICT DO UPDATE, independientemente de si el UPDATE afectó o no a alguna fila (e independientemente de si se tomó alguna vez la ruta alternativa de UPDATE). Un INSERT con una cláusula ON CONFLICT DO UPDATE ejecutará primero los disparadores BEFORE INSERT a nivel de sentencia, luego los disparadores BEFORE UPDATE a nivel de sentencia, seguidos por los disparadores AFTER UPDATE a nivel de sentencia y finalmente los disparadores AFTER INSERT a nivel de sentencia.

Una sentencia dirigida a una tabla padre en una jerarquía de herencia o particionamiento no hace que se activen los disparadores a nivel de sentencia de las tablas hijas afectadas; sólo se activan los disparadores a nivel de sentencia de la tabla padre. Sin embargo, los disparadores a nivel de fila de cualquier tabla hija afectada sí se activarán.

Si un UPDATE en una tabla particionada hace que una fila se mueva a otra partición, se realizará como un DELETE de la partición original seguido de un INSERT en la nueva partición. En este caso, todos los disparadores BEFORE UPDATE a nivel de fila y todos los disparadores BEFORE DELETE a nivel de fila se activan en la partición original. Luego, todos los disparadores BEFORE INSERT a nivel de fila se activan en la partición de destino. Se debe considerar la posibilidad de resultados sorprendentes cuando todos estos disparadores afectan a la fila que se está moviendo. En lo que respecta a los disparadores AFTER ROW, se aplican los disparadores AFTER DELETE y AFTER INSERT; pero los disparadores AFTER UPDATE no se aplican porque el UPDATE se ha convertido en un DELETE y un INSERT. En lo que respecta a los disparadores a nivel de sentencia, ninguno de los disparadores de DELETE o INSERT se activa, incluso si ocurre movimiento de filas; sólo se activarán los disparadores de UPDATE definidos en la tabla de destino utilizada en la sentencia UPDATE.

No se definen disparadores separados para MERGE. En su lugar, se activan disparadores de UPDATE, DELETE e INSERT a nivel de sentencia o de fila dependiendo de (para disparadores a nivel de sentencia) qué acciones se especifican en la consulta MERGE y (para disparadores a nivel de fila) qué acciones se realizan.

Durante la ejecución de un comando MERGE, se activan disparadores BEFORE y AFTER a nivel de sentencia para los eventos especificados en las acciones del comando MERGE, independientemente de si la acción se realiza o no en última instancia. Esto es lo mismo que una sentencia UPDATE que no actualiza ninguna fila, y aún así se activan los disparadores a nivel de sentencia. Los disparadores a nivel de fila se activan sólo cuando una fila es realmente actualizada, insertada o eliminada. Por lo tanto, es perfectamente legal que mientras se activan disparadores a nivel de sentencia para ciertos tipos de acciones, no se activen disparadores a nivel de fila para el mismo tipo de acción.

Las funciones de disparo invocadas por disparadores por sentencia siempre deben devolver NULL. Las funciones de disparo invocadas por disparadores por fila pueden devolver una fila de la tabla (un valor de tipo HeapTuple) al ejecutor invocador, si así lo deciden. Un disparador a nivel de fila activado antes de una operación tiene las siguientes opciones:

Un disparador BEFORE a nivel de fila que no tenga la intención de causar ninguno de estos comportamientos debe tener cuidado de devolver como resultado la misma fila que se le pasó (es decir, la fila NEW para los disparadores INSERT y UPDATE, la fila OLD para los disparadores DELETE).

Un disparador INSTEAD OF a nivel de fila debe devolver NULL para indicar que no modificó ningún dato de las tablas base subyacentes de la vista, o debe devolver la fila de la vista que se le pasó (la fila NEW para las operaciones INSERT y UPDATE, o la fila OLD para las operaciones DELETE). Se utiliza un valor de retorno que no sea nulo para indicar que el disparador realizó las modificaciones de datos necesarias en la vista. Esto hará que se incremente el conteo del número de filas afectadas por el comando. Sólo para operaciones INSERT y UPDATE, el disparador puede modificar la fila NEW antes de devolverla. Esto cambiará los datos devueltos por INSERT RETURNING o UPDATE RETURNING, y es útil cuando la vista no mostrará exactamente los mismos datos que se proporcionaron.

El valor de retorno se ignora para los disparadores a nivel de fila activados después de una operación, por lo que pueden devolver NULL.

Se aplican algunas consideraciones para las columnas generadas. Las columnas generadas almacenadas se calculan después de los disparadores BEFORE y antes de los disparadores AFTER. Por lo tanto, el valor generado se puede inspeccionar en los disparadores AFTER. En los disparadores BEFORE, la fila OLD contiene el valor generado antiguo, como era de esperar, pero la fila NEW aún no contiene el nuevo valor generado y no debe accederse. En la interfaz del lenguaje C, el contenido de la columna no está definido en este punto; un lenguaje de programación de nivel superior debería impedir el acceso a una columna generada almacenada en la fila NEW en un disparador BEFORE. Los cambios en el valor de una columna generada en un disparador BEFORE se ignoran y se sobrescribirán. Las columnas generadas virtuales nunca se calculan cuando se activan los disparadores. En la interfaz del lenguaje C, su contenido no está definido en una función de disparo. Los lenguajes de programación de nivel superior deberían impedir el acceso a las columnas generadas virtuales en los disparadores.

Si se define más de un disparador para el mismo evento en la misma relación, los disparadores se activarán en orden alfabético por el nombre del disparador. En el caso de los disparadores BEFORE y INSTEAD OF, la fila posiblemente modificada devuelta por cada disparador se convierte en la entrada para el siguiente disparador. Si algún disparador BEFORE o INSTEAD OF devuelve NULL, la operación se abandona para esa fila y no se activan los disparadores subsiguientes (para esa fila).

La definición de un disparador también puede especificar una condición booleana WHEN, que se evaluará para ver si se debe activar el disparador. En los disparadores a nivel de fila, la condición WHEN puede examinar los valores antiguos y/o nuevos de las columnas de la fila. (Los disparadores a nivel de sentencia también pueden tener condiciones WHEN, aunque la característica no es tan útil para ellos). En un disparador BEFORE, la condición WHEN se evalúa justo antes de que la función se ejecute o se fuera a ejecutar, por lo que usar WHEN no es materialmente diferente de probar la misma condición al comienzo de la función del disparador. Sin embargo, en un disparador AFTER, la condición WHEN se evalúa justo después de que ocurra la actualización de la fila, y determina si un evento se pone en cola para activar el disparador al final de la sentencia. Por lo tanto, cuando la condición WHEN de un disparador AFTER no devuelve true, no es necesario poner en cola un evento ni volver a recuperar la fila al final de la sentencia. Esto puede resultar en aceleraciones significativas en sentencias que modifican muchas filas, si el disparador sólo necesita activarse para unas pocas de las filas. Los disparadores INSTEAD OF no admiten condiciones WHEN.

Normalmente, los disparadores BEFORE a nivel de fila se utilizan para verificar o modificar los datos que se insertarán o actualizarán. Por ejemplo, un disparador BEFORE podría usarse para insertar la hora actual en una columna de tipo timestamp, o para verificar que dos elementos de la fila sean consistentes. Los disparadores AFTER a nivel de fila se utilizan de manera más lógica para propagar las actualizaciones a otras tablas, o para realizar comprobaciones de consistencia con respecto a otras tablas. La razón de esta división del trabajo es que un disparador AFTER puede estar seguro de que está viendo el valor final de la fila, mientras que un disparador BEFORE no puede; podría haber otros disparadores BEFORE activándose después de él. Si no tienes una razón específica para hacer que un disparador sea BEFORE o AFTER, el caso BEFORE es más eficiente, ya que la información sobre la operación no tiene que guardarse hasta el final de la sentencia.

Si una función de disparo ejecuta comandos SQL, estos comandos podrían activar disparadores nuevamente. Esto se conoce como disparadores en cascada (cascading triggers). No existe una limitación directa sobre el número de niveles de cascada. Es posible que las cascadas causen una invocación recursiva del mismo disparador; por ejemplo, un disparador de INSERT podría ejecutar un comando que inserte una fila adicional en la misma tabla, lo que haría que el disparador de INSERT se activara nuevamente. Es responsabilidad del programador del disparador evitar la recursión infinita en tales escenarios.

Si una restricción de clave foránea especifica acciones referenciales (es decir, actualizaciones o eliminaciones en cascada), esas acciones se realizan mediante comandos SQL ordinarios UPDATE o DELETE en la tabla de referencia. En particular, cualquier disparador que exista en la tabla de referencia se activará para esos cambios. Si dicho disparador modifica o bloquea el efecto de uno de estos comandos, el resultado final podría ser romper la integridad referencial. Es responsabilidad del programador del disparador evitar eso.

Cuando se define un disparador, se pueden especificar argumentos para él. El propósito de incluir argumentos en la definición del disparador es permitir que diferentes disparadores con requisitos similares llamen a la misma función. Como ejemplo, podría haber una función de disparo generalizada que tome como argumentos dos nombres de columna y coloque al usuario actual en una y la marca de tiempo actual en la otra. Escrita correctamente, esta función de disparo sería independiente de la tabla específica en la que se activa. De modo que la misma función podría usarse para eventos INSERT en cualquier tabla con columnas adecuadas, para rastrear automáticamente la creación de registros en una tabla de transacciones, por ejemplo. También podría usarse para rastrear eventos de última actualización si se define como un disparador de UPDATE.

Cada lenguaje de programación que admite disparadores tiene su propio método para hacer que los datos de entrada del disparador estén disponibles para la función del disparador. Estos datos de entrada incluyen el tipo de evento del disparador (por ejemplo, INSERT o UPDATE), así como cualquier argumento que se haya enumerado en CREATE TRIGGER. Para un disparador a nivel de fila, los datos de entrada también incluyen la fila NEW para los disparadores INSERT y UPDATE, y/o la fila OLD para los disparadores UPDATE y DELETE.

Por defecto, los disparadores a nivel de sentencia no tienen ninguna forma de examinar la(s) fila(s) individual(es) modificada(s) por la sentencia. Pero un disparador AFTER STATEMENT puede solicitar que se creen tablas de transición (transition tables) para que los conjuntos de filas afectadas estén disponibles para el disparador. Los disparadores AFTER ROW también pueden solicitar tablas de transición, de modo que puedan ver los cambios totales en la tabla, así como el cambio en la fila individual para la que se están activando actualmente. El método para examinar las tablas de transición depende nuevamente del lenguaje de programación que se esté utilizando, pero el enfoque típico es hacer que las tablas de transición actúen como tablas temporales de sólo lectura a las que se puede acceder mediante comandos SQL emitidos dentro de la función del disparador.