Módulo 07: Triggers (Disparadores)

Objetivos del módulo

  • Entender qué son los triggers y cuándo utilizarlos
  • Crear triggers DML a nivel de sentencia y a nivel de fila
  • Dominar los pseudoregistros :NEW y :OLD
  • Utilizar la cláusula WHEN para disparos condicionales
  • Crear triggers INSTEAD OF sobre vistas
  • Desarrollar triggers de eventos del sistema y DDL
  • Gestionar triggers compuestos (compound triggers)
  • Habilitar, deshabilitar y eliminar triggers
  • Aplicar buenas prácticas y evitar errores comunes

1. ¿Qué es un trigger?

Un trigger (disparador) es un bloque PL/SQL con nombre almacenado en la base de datos que se ejecuta automáticamente cuando ocurre un evento determinado (INSERT, UPDATE, DELETE, DDL, etc.).

A diferencia de los procedimientos y funciones, no se invocan manualmente: Oracle los ejecuta de forma implícita.

Característica Procedimiento Trigger
Invocación Manual (EXEC, CALL) Automática (al ocurrir el evento)
Parámetros No
RETURN Solo funciones No
COMMIT / ROLLBACK directo No (salvo triggers autónomos)
¿Se puede deshabilitar? No aplica Sí (ALTER TRIGGER ... DISABLE)

📘 Concepto: Los triggers son ideales para auditoría, validaciones de negocio complejas, mantenimiento automático de datos derivados y replicación. No deben usarse como reemplazo de constraints simples ni para lógica de negocio que pertenezca a procedimientos.


2. Tipos de triggers

Oracle soporta varios tipos de triggers:

Tipo Evento Uso principal
DML INSERT, UPDATE, DELETE sobre tablas Auditoría, validación, datos derivados
INSTEAD OF INSERT, UPDATE, DELETE sobre vistas Hacer modificables vistas complejas
DDL CREATE, ALTER, DROP Auditar cambios en el esquema
De sistema LOGON, LOGOFF, STARTUP, SHUTDOWN Seguridad, auditoría de sesiones
Compuesto DML (múltiples puntos de disparo) Evitar errores de tabla mutante

3. Triggers DML

3.1. Sintaxis general

CREATE [OR REPLACE] TRIGGER nombre_trigger
    {BEFORE | AFTER | INSTEAD OF}
    {INSERT | UPDATE [OF columna] | DELETE}
    [OR {INSERT | UPDATE [OF columna] | DELETE}]
    ON nombre_tabla
    [REFERENCING OLD AS old NEW AS new]
    [FOR EACH ROW]
    [WHEN (condicion)]
DECLARE
    -- Declaraciones opcionales
BEGIN
    -- Código del trigger
EXCEPTION
    -- Manejo de excepciones
END [nombre_trigger];
/

3.2. Momento de disparo: BEFORE vs AFTER

Momento Descripción Uso típico
BEFORE Se ejecuta antes de la operación DML Validar o modificar datos antes de que se guarden
AFTER Se ejecuta después de la operación DML Auditoría, actualizaciones en cascada

3.3. Nivel: sentencia vs fila

Nivel Sintaxis Se dispara…
Sentencia (sin FOR EACH ROW) Una vez por sentencia DML
Fila FOR EACH ROW Una vez por cada fila afectada

💡 Truco: Si un UPDATE modifica 100 filas, un trigger a nivel de sentencia se ejecuta 1 vez, mientras que un trigger FOR EACH ROW se ejecuta 100 veces.


4. Triggers a nivel de sentencia

Se ejecutan una sola vez por cada sentencia DML, independientemente del número de filas afectadas:

-- Trigger que impide operaciones fuera de horario laboral
CREATE OR REPLACE TRIGGER trg_horario_laboral
    BEFORE INSERT OR UPDATE OR DELETE
    ON conductor
BEGIN
    IF TO_CHAR(SYSDATE, 'DY', 'NLS_DATE_LANGUAGE=ENGLISH') IN ('SAT', 'SUN')
       OR TO_NUMBER(TO_CHAR(SYSDATE, 'HH24')) NOT BETWEEN 8 AND 18 THEN
        RAISE_APPLICATION_ERROR(-20001, 
            'Solo se permiten modificaciones en horario laboral (L-V, 8:00-18:00)');
    END IF;
END trg_horario_laboral;
/
-- Trigger de auditoría a nivel de sentencia
CREATE OR REPLACE TRIGGER trg_audit_operacion_conductor
    AFTER INSERT OR UPDATE OR DELETE
    ON conductor
DECLARE
    v_operacion VARCHAR2(10);
BEGIN
    IF INSERTING THEN
        v_operacion := 'INSERT';
    ELSIF UPDATING THEN
        v_operacion := 'UPDATE';
    ELSIF DELETING THEN
        v_operacion := 'DELETE';
    END IF;
    
    INSERT INTO log_auditoria (accion)
    VALUES ('Operación ' || v_operacion || ' sobre CONDUCTOR por ' || USER);
END trg_audit_operacion_conductor;
/

📘 Concepto: Los predicados condicionales INSERTING, UPDATING y DELETING permiten saber qué operación DML disparó el trigger. Se pueden usar en triggers que responden a múltiples eventos (INSERT OR UPDATE OR DELETE).


5. Triggers a nivel de fila y pseudoregistros :NEW / :OLD

Los triggers FOR EACH ROW tienen acceso a los pseudoregistros :NEW y :OLD:

Pseudoregistro INSERT UPDATE DELETE
:OLD NULL (no hay fila previa) Valor antes del cambio Valor de la fila eliminada
:NEW Valor que se va a insertar Valor después del cambio NULL (no hay fila nueva)

5.1. Trigger BEFORE INSERT - Asignar valores automáticos

CREATE OR REPLACE TRIGGER trg_conductor_antes_insert
    BEFORE INSERT
    ON conductor
    FOR EACH ROW
BEGIN
    -- Forzar nombre en mayúsculas
    :NEW.nombre := UPPER(:NEW.nombre);
    
    -- Si no se indica fecha de carnet, usar la fecha actual
    IF :NEW.fec_carnet IS NULL THEN
        :NEW.fec_carnet := SYSDATE;
    END IF;
END trg_conductor_antes_insert;
/

Prueba:

INSERT INTO conductor (DNI, nombre, fec_nacimiento)
VALUES ('99999999Z', 'juan garcía', TO_DATE('15/06/1990', 'DD/MM/YYYY'));

-- Verificar que se transformó a mayúsculas y se puso fecha de carnet
SELECT nombre, fec_carnet FROM conductor WHERE DNI = '99999999Z';
-- Resultado: JUAN GARCÍA, fecha de hoy

⚠️ Importante: Solo puedes modificar :NEW en triggers BEFORE. En triggers AFTER, los valores de :NEW son de solo lectura porque la fila ya se ha guardado.

5.2. Trigger AFTER - Auditoría detallada

-- Tabla de auditoría
CREATE TABLE aud_conductor (
    aud_id      NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    aud_fecha   TIMESTAMP DEFAULT SYSTIMESTAMP,
    aud_usuario VARCHAR2(30) DEFAULT USER,
    aud_accion  VARCHAR2(10),
    dni         VARCHAR2(15),
    nombre_old  VARCHAR2(100),
    nombre_new  VARCHAR2(100)
);

-- Trigger de auditoría
CREATE OR REPLACE TRIGGER trg_audit_conductor_detalle
    AFTER INSERT OR UPDATE OR DELETE
    ON conductor
    FOR EACH ROW
DECLARE
    v_accion VARCHAR2(10);
BEGIN
    IF INSERTING THEN
        v_accion := 'INSERT';
    ELSIF UPDATING THEN
        v_accion := 'UPDATE';
    ELSIF DELETING THEN
        v_accion := 'DELETE';
    END IF;
    
    INSERT INTO aud_conductor (aud_accion, dni, nombre_old, nombre_new)
    VALUES (v_accion,
            COALESCE(:NEW.DNI, :OLD.DNI),
            :OLD.nombre,
            :NEW.nombre);
END trg_audit_conductor_detalle;
/

Prueba:

-- Insertar un registro
INSERT INTO conductor (DNI, nombre, fec_nacimiento, fec_carnet)
VALUES ('88888888X', 'ANA LÓPEZ', TO_DATE('01/01/1985', 'DD/MM/YYYY'), SYSDATE);

-- Actualizar
UPDATE conductor SET nombre = 'ANA LÓPEZ RUIZ' WHERE DNI = '88888888X';

-- Eliminar
DELETE FROM conductor WHERE DNI = '88888888X';

-- Ver auditoría
SELECT aud_accion, dni, nombre_old, nombre_new, aud_fecha
FROM aud_conductor
ORDER BY aud_id;

5.3. Trigger con UPDATE OF (columnas específicas)

-- Solo se dispara cuando se modifica el importe de una multa
CREATE OR REPLACE TRIGGER trg_multa_cambio_importe
    BEFORE UPDATE OF importe
    ON multa
    FOR EACH ROW
BEGIN
    IF :NEW.importe > :OLD.importe * 2 THEN
        RAISE_APPLICATION_ERROR(-20010, 
            'No se permite duplicar el importe. Anterior: ' || :OLD.importe || 
            ', Nuevo: ' || :NEW.importe);
    END IF;
    
    DBMS_OUTPUT.PUT_LINE('Importe modificado de ' || :OLD.importe || 
                         ' a ' || :NEW.importe);
END trg_multa_cambio_importe;
/

6. Cláusula WHEN

La cláusula WHEN permite condicionar el disparo del trigger sin entrar en el bloque PL/SQL (más eficiente):

-- Solo auditar cambios en conductores nacidos antes de 1980
CREATE OR REPLACE TRIGGER trg_audit_conductor_antiguos
    AFTER UPDATE
    ON conductor
    FOR EACH ROW
    WHEN (OLD.fec_nacimiento < TO_DATE('01/01/1980', 'DD/MM/YYYY'))
BEGIN
    INSERT INTO aud_conductor (aud_accion, dni, nombre_old, nombre_new)
    VALUES ('UPDATE', :OLD.DNI, :OLD.nombre, :NEW.nombre);
END trg_audit_conductor_antiguos;
/

💡 Truco: Dentro de la cláusula WHEN, los pseudoregistros se referencian sin los dos puntos: OLD.columna y NEW.columna (no :OLD ni :NEW). Los dos puntos solo se usan dentro del bloque BEGIN...END.


7. INSTEAD OF triggers (sobre vistas)

Los triggers INSTEAD OF permiten hacer modificable una vista que normalmente sería de solo lectura (por ejemplo, vistas con JOINs):

-- Vista que combina conductor y vehículo
CREATE OR REPLACE VIEW vw_conductor_vehiculo AS
SELECT c.DNI, c.nombre, v.matricula, v.marca, v.color
FROM conductor c
JOIN vehiculo v ON c.DNI = v.DNI;

-- Trigger INSTEAD OF para permitir INSERT en la vista
CREATE OR REPLACE TRIGGER trg_iof_conductor_vehiculo
    INSTEAD OF INSERT
    ON vw_conductor_vehiculo
    FOR EACH ROW
BEGIN
    -- Insertar en conductor si no existe
    MERGE INTO conductor c
    USING (SELECT :NEW.DNI AS dni FROM DUAL) src
    ON (c.DNI = src.dni)
    WHEN NOT MATCHED THEN
        INSERT (DNI, nombre, fec_nacimiento, fec_carnet)
        VALUES (:NEW.DNI, :NEW.nombre, SYSDATE, SYSDATE);
    
    -- Insertar el vehículo
    INSERT INTO vehiculo (matricula, marca, color, DNI)
    VALUES (:NEW.matricula, :NEW.marca, :NEW.color, :NEW.DNI);
END trg_iof_conductor_vehiculo;
/

Prueba:

-- Insertar a través de la vista
INSERT INTO vw_conductor_vehiculo (DNI, nombre, matricula, marca, color)
VALUES ('77777777Y', 'Pedro Ruiz', '9999ZZZ', 'Tesla', 'Blanco');

-- Verificar que se insertó en ambas tablas
SELECT * FROM conductor WHERE DNI = '77777777Y';
SELECT * FROM vehiculo WHERE matricula = '9999ZZZ';

📘 Concepto: Los triggers INSTEAD OF reemplazan la operación original. Oracle no ejecuta el INSERT/UPDATE/DELETE sobre la vista; en su lugar, ejecuta el código del trigger. Solo se pueden crear sobre vistas, no sobre tablas.


8. Triggers DDL y de sistema

8.1. Trigger DDL - Auditar cambios de esquema

-- Tabla para auditar DDL
CREATE TABLE aud_ddl (
    aud_id        NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    aud_fecha     TIMESTAMP DEFAULT SYSTIMESTAMP,
    aud_usuario   VARCHAR2(30),
    aud_evento    VARCHAR2(30),
    aud_tipo_obj  VARCHAR2(30),
    aud_nombre_obj VARCHAR2(128)
);

-- Trigger que audita cualquier operación DDL en el esquema
CREATE OR REPLACE TRIGGER trg_audit_ddl
    AFTER DDL
    ON SCHEMA
BEGIN
    INSERT INTO aud_ddl (aud_usuario, aud_evento, aud_tipo_obj, aud_nombre_obj)
    VALUES (
        ORA_LOGIN_USER,
        ORA_SYSEVENT,
        ORA_DICT_OBJ_TYPE,
        ORA_DICT_OBJ_NAME
    );
END trg_audit_ddl;
/

8.2. Trigger para impedir DROP de tablas

CREATE OR REPLACE TRIGGER trg_proteger_tablas
    BEFORE DROP
    ON SCHEMA
BEGIN
    IF ORA_DICT_OBJ_TYPE = 'TABLE' THEN
        RAISE_APPLICATION_ERROR(-20050,
            'No está permitido eliminar tablas. Tabla: ' || ORA_DICT_OBJ_NAME ||
            '. Contacte al DBA.');
    END IF;
END trg_proteger_tablas;
/

8.3. Trigger de LOGON

-- Registrar cada inicio de sesión
CREATE OR REPLACE TRIGGER trg_audit_logon
    AFTER LOGON
    ON SCHEMA
BEGIN
    INSERT INTO log_auditoria (accion)
    VALUES ('LOGON de ' || USER || ' desde ' || SYS_CONTEXT('USERENV', 'IP_ADDRESS') ||
            ' a las ' || TO_CHAR(SYSDATE, 'DD/MM/YYYY HH24:MI:SS'));
    COMMIT;
END trg_audit_logon;
/

⚠️ Importante: Los triggers de LOGON/LOGOFF requieren el privilegio ADMINISTER DATABASE TRIGGER si se crean a nivel de base de datos (ON DATABASE en lugar de ON SCHEMA).


9. Triggers compuestos (Compound Triggers)

Los triggers compuestos (disponibles desde Oracle 11g) permiten definir múltiples puntos de disparo en un solo trigger. Son la solución recomendada para evitar el error de tabla mutante (ORA-04091):

CREATE OR REPLACE TRIGGER trg_compound_multa
    FOR INSERT OR UPDATE
    ON multa
    COMPOUND TRIGGER

    -- Tipo y colección para almacenar datos entre puntos de disparo
    TYPE t_multa_rec IS RECORD (
        matricula VARCHAR2(20),
        importe   NUMBER
    );
    TYPE t_multas_tab IS TABLE OF t_multa_rec INDEX BY PLS_INTEGER;
    v_multas t_multas_tab;
    v_idx    PLS_INTEGER := 0;

    -- BEFORE STATEMENT: inicialización
    BEFORE STATEMENT IS
    BEGIN
        v_multas.DELETE;
        v_idx := 0;
    END BEFORE STATEMENT;

    -- BEFORE EACH ROW: capturar datos de cada fila
    BEFORE EACH ROW IS
    BEGIN
        -- Validar que el importe sea positivo
        IF :NEW.importe <= 0 THEN
            RAISE_APPLICATION_ERROR(-20020, 'El importe de la multa debe ser positivo');
        END IF;
    END BEFORE EACH ROW;

    -- AFTER EACH ROW: almacenar datos para procesamiento posterior
    AFTER EACH ROW IS
    BEGIN
        v_idx := v_idx + 1;
        v_multas(v_idx).matricula := :NEW.matricula;
        v_multas(v_idx).importe   := :NEW.importe;
    END AFTER EACH ROW;

    -- AFTER STATEMENT: procesamiento final (puede leer la tabla sin error de mutante)
    AFTER STATEMENT IS
        v_total_vehiculo NUMBER;
    BEGIN
        FOR i IN 1..v_multas.COUNT LOOP
            SELECT SUM(importe) INTO v_total_vehiculo
            FROM multa
            WHERE matricula = v_multas(i).matricula;
            
            DBMS_OUTPUT.PUT_LINE(
                'Vehículo ' || v_multas(i).matricula || 
                ': Total multas acumuladas = ' || v_total_vehiculo || ' €');
        END LOOP;
    END AFTER STATEMENT;

END trg_compound_multa;
/

📘 Concepto: El error de tabla mutante (ORA-04091) ocurre cuando un trigger FOR EACH ROW intenta leer o consultar la misma tabla sobre la que se disparó. El compound trigger lo resuelve almacenando los datos en la fase AFTER EACH ROW y consultando la tabla en la fase AFTER STATEMENT, donde ya no está en estado de mutación.


10. Gestión de triggers

10.1. Habilitar y deshabilitar

-- Deshabilitar un trigger específico
ALTER TRIGGER trg_horario_laboral DISABLE;

-- Habilitar un trigger específico
ALTER TRIGGER trg_horario_laboral ENABLE;

-- Deshabilitar TODOS los triggers de una tabla
ALTER TABLE conductor DISABLE ALL TRIGGERS;

-- Habilitar TODOS los triggers de una tabla
ALTER TABLE conductor ENABLE ALL TRIGGERS;

10.2. Eliminar triggers

DROP TRIGGER trg_horario_laboral;
DROP TRIGGER trg_audit_conductor_detalle;

10.3. Consultar triggers en el diccionario de datos

-- Ver todos tus triggers
SELECT trigger_name, trigger_type, triggering_event, table_name, status
FROM user_triggers
ORDER BY table_name, trigger_name;

-- Ver el código fuente de un trigger
SELECT trigger_body
FROM user_triggers
WHERE trigger_name = 'TRG_AUDIT_CONDUCTOR_DETALLE';

-- Ver triggers de una tabla específica
SELECT trigger_name, trigger_type, triggering_event, status
FROM user_triggers
WHERE table_name = 'CONDUCTOR'
ORDER BY trigger_name;

-- Ver errores de compilación
SHOW ERRORS TRIGGER trg_audit_conductor_detalle;
-- O bien:
SELECT line, position, text
FROM user_errors
WHERE name = 'TRG_AUDIT_CONDUCTOR_DETALLE'
ORDER BY line;

11. Orden de ejecución de triggers

Cuando hay múltiples triggers sobre una misma tabla y operación, Oracle los ejecuta en este orden:

  1. BEFORE a nivel de sentencia
  2. Para cada fila afectada:
    • BEFORE a nivel de fila
    • Ejecución de la sentencia DML sobre la fila
    • AFTER a nivel de fila
  3. AFTER a nivel de sentencia
Sentencia DML
├── 1. BEFORE STATEMENT trigger(s)
├── Para cada fila:
│   ├── 2. BEFORE EACH ROW trigger(s)
│   ├── 3. Ejecutar DML en la fila
│   └── 4. AFTER EACH ROW trigger(s)
└── 5. AFTER STATEMENT trigger(s)

💡 Truco: Si hay varios triggers del mismo tipo y momento, el orden entre ellos es indeterminado (no se garantiza). Si necesitas un orden específico, usa FOLLOWS (Oracle 11g+):

CREATE OR REPLACE TRIGGER trg_segundo
    BEFORE INSERT ON conductor
    FOR EACH ROW
    FOLLOWS trg_primero
BEGIN
    -- Se ejecuta después de trg_primero
    NULL;
END;
/

12. Buenas prácticas

Práctica Motivo
Triggers cortos y sencillos Código largo en triggers dificulta el debugging y el mantenimiento
No usar COMMIT/ROLLBACK El trigger forma parte de la transacción que lo disparó
Evitar lógica de negocio compleja Esa lógica pertenece a procedimientos y paquetes
No llamar a otros triggers en cadena Los triggers en cascada son difíciles de depurar (máx. 32 niveles)
Usar compound triggers para tabla mutante Evita ORA-04091 de forma limpia
Nombrar con prefijo trg_ Facilita la identificación en el diccionario de datos
Documentar el propósito Un trigger mal documentado se convierte en código misterioso
Controlar excepciones Un error no manejado en un trigger aborta la operación DML completa

13. Ejercicios prácticos

Ejercicio 1: Auditoría básica de vehículos

Crea una tabla de auditoría y un trigger que registre toda operación INSERT, UPDATE y DELETE sobre la tabla vehiculo:

-- Tabla de auditoría
CREATE TABLE aud_vehiculo (
    aud_id      NUMBER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    aud_fecha   TIMESTAMP DEFAULT SYSTIMESTAMP,
    aud_usuario VARCHAR2(30) DEFAULT USER,
    aud_accion  VARCHAR2(10),
    matricula   VARCHAR2(20),
    marca_old   VARCHAR2(50),
    marca_new   VARCHAR2(50),
    color_old   VARCHAR2(50),
    color_new   VARCHAR2(50)
);

-- Trigger
CREATE OR REPLACE TRIGGER trg_audit_vehiculo
    AFTER INSERT OR UPDATE OR DELETE
    ON vehiculo
    FOR EACH ROW
DECLARE
    v_accion VARCHAR2(10);
BEGIN
    IF INSERTING THEN v_accion := 'INSERT';
    ELSIF UPDATING THEN v_accion := 'UPDATE';
    ELSE v_accion := 'DELETE';
    END IF;
    
    INSERT INTO aud_vehiculo (aud_accion, matricula, marca_old, marca_new, color_old, color_new)
    VALUES (v_accion,
            COALESCE(:NEW.matricula, :OLD.matricula),
            :OLD.marca, :NEW.marca,
            :OLD.color, :NEW.color);
END trg_audit_vehiculo;
/

Ejercicio 2: Validación de negocio con trigger BEFORE

Crea un trigger que impida que un conductor tenga el carnet con fecha anterior a su nacimiento o siendo menor de 18 años:

CREATE OR REPLACE TRIGGER trg_validar_conductor
    BEFORE INSERT OR UPDATE
    ON conductor
    FOR EACH ROW
BEGIN
    IF :NEW.fec_carnet IS NOT NULL AND :NEW.fec_nacimiento IS NOT NULL THEN
        -- El carnet no puede ser anterior al nacimiento
        IF :NEW.fec_carnet < :NEW.fec_nacimiento THEN
            RAISE_APPLICATION_ERROR(-20030, 
                'La fecha del carnet no puede ser anterior a la fecha de nacimiento');
        END IF;
        
        -- Debe tener al menos 18 años al obtener el carnet
        IF MONTHS_BETWEEN(:NEW.fec_carnet, :NEW.fec_nacimiento) / 12 < 18 THEN
            RAISE_APPLICATION_ERROR(-20031, 
                'El conductor debe tener al menos 18 años para obtener el carnet');
        END IF;
    END IF;
END trg_validar_conductor;
/

-- Prueba (debe fallar)
INSERT INTO conductor (DNI, nombre, fec_nacimiento, fec_carnet)
VALUES ('11111111A', 'MENOR', TO_DATE('01/01/2010', 'DD/MM/YYYY'), SYSDATE);
-- Error: El conductor debe tener al menos 18 años

-- Prueba (debe funcionar)
INSERT INTO conductor (DNI, nombre, fec_nacimiento, fec_carnet)
VALUES ('22222222B', 'MAYOR', TO_DATE('01/01/1990', 'DD/MM/YYYY'), SYSDATE);

Ejercicio 3: Trigger INSTEAD OF sobre vista

Crea una vista que muestre las multas con el nombre del conductor y un trigger que permita insertar multas usando el DNI del conductor:

CREATE OR REPLACE VIEW vw_multas_detalle AS
SELECT m.fecha_multa, m.hora_multa, m.matricula, m.importe,
       c.nombre AS conductor, i.descripcion AS infraccion
FROM multa m
JOIN vehiculo v ON m.matricula = v.matricula
JOIN conductor c ON v.DNI = c.DNI
JOIN infraccion i ON m.cod_infraccion = i.cod_infraccion;

CREATE OR REPLACE TRIGGER trg_iof_insertar_multa
    INSTEAD OF INSERT
    ON vw_multas_detalle
    FOR EACH ROW
DECLARE
    v_matricula vehiculo.matricula%TYPE;
    v_cod_inf   infraccion.cod_infraccion%TYPE;
BEGIN
    -- Buscar la matrícula por el nombre del conductor
    SELECT matricula INTO v_matricula
    FROM vehiculo v JOIN conductor c ON v.DNI = c.DNI
    WHERE UPPER(c.nombre) LIKE '%' || UPPER(:NEW.conductor) || '%'
    AND ROWNUM = 1;
    
    -- Buscar el código de infracción por la descripción
    SELECT cod_infraccion INTO v_cod_inf
    FROM infraccion
    WHERE UPPER(descripcion) LIKE '%' || UPPER(:NEW.infraccion) || '%'
    AND ROWNUM = 1;
    
    -- Insertar la multa
    INSERT INTO multa (fecha_multa, hora_multa, matricula, cod_infraccion, importe)
    VALUES (:NEW.fecha_multa, :NEW.hora_multa, v_matricula, v_cod_inf, :NEW.importe);
EXCEPTION
    WHEN NO_DATA_FOUND THEN
        RAISE_APPLICATION_ERROR(-20040, 'No se encontró conductor o infracción');
END trg_iof_insertar_multa;
/

Ejercicio 4: Compound trigger para evitar tabla mutante

Crea un compound trigger que valide que el total de multas de un vehículo no supere los 10.000 €:

CREATE OR REPLACE TRIGGER trg_limite_multas_vehiculo
    FOR INSERT OR UPDATE OF importe
    ON multa
    COMPOUND TRIGGER

    TYPE t_matriculas IS TABLE OF multa.matricula%TYPE INDEX BY PLS_INTEGER;
    v_matriculas t_matriculas;
    v_idx PLS_INTEGER := 0;

    AFTER EACH ROW IS
    BEGIN
        v_idx := v_idx + 1;
        v_matriculas(v_idx) := :NEW.matricula;
    END AFTER EACH ROW;

    AFTER STATEMENT IS
        v_total NUMBER;
    BEGIN
        FOR i IN 1..v_matriculas.COUNT LOOP
            SELECT NVL(SUM(importe), 0) INTO v_total
            FROM multa
            WHERE matricula = v_matriculas(i);
            
            IF v_total > 10000 THEN
                RAISE_APPLICATION_ERROR(-20060,
                    'El vehículo ' || v_matriculas(i) || 
                    ' supera el límite de 10.000 € en multas (Total: ' || v_total || ' €)');
            END IF;
        END LOOP;
    END AFTER STATEMENT;

END trg_limite_multas_vehiculo;
/

Resumen

Concepto Detalle
Trigger Bloque PL/SQL que se ejecuta automáticamente ante un evento
BEFORE / AFTER Momento de disparo: antes o después de la operación
FOR EACH ROW Trigger a nivel de fila (se ejecuta por cada fila afectada)
Nivel de sentencia Sin FOR EACH ROW; se ejecuta una vez por sentencia
:NEW Pseudoregistro con los valores nuevos (INSERT/UPDATE)
:OLD Pseudoregistro con los valores anteriores (UPDATE/DELETE)
INSERTING / UPDATING / DELETING Predicados para saber qué operación disparó el trigger
UPDATE OF columna Dispara solo cuando se modifica una columna específica
WHEN (condición) Condición adicional para el disparo (sin : en OLD/NEW)
INSTEAD OF Trigger sobre vistas; reemplaza la operación original
Trigger DDL Disparado por CREATE, ALTER, DROP en el esquema
Trigger LOGON/LOGOFF Disparado al conectarse/desconectarse de la BD
Compound trigger Trigger con múltiples puntos de disparo; resuelve tabla mutante
FOLLOWS Establece orden entre triggers del mismo tipo
ALTER TRIGGER … DISABLE/ENABLE Habilita o deshabilita un trigger
USER_TRIGGERS Vista del diccionario para consultar triggers
Tabla mutante (ORA-04091) Error al consultar la tabla desde un trigger FOR EACH ROW