CodeGym /Cursos /SQL SELF /Interacción de triggers con funciones PL/pgSQL: OLD, NEW,...

Interacción de triggers con funciones PL/pgSQL: OLD, NEW, TG_OP

SQL SELF
Nivel 57 , Lección 4
Disponible

Los triggers en PostgreSQL no solo permiten lanzar funciones en respuesta a ciertas acciones, sino que también pasan unas variables mágicas a esas funciones. Gracias a estas variables puedes saber qué datos había en la tabla antes de la operación, qué quedó después y qué operación se realizó realmente.

  • OLD — contiene los datos antiguos de la fila de la tabla antes de la operación. Se usa en triggers para UPDATE y DELETE, porque en el caso de INSERT simplemente no existe nada "viejo".
  • NEW — contiene los datos nuevos de la fila de la tabla después de la operación. Se usa en triggers para INSERT y UPDATE.
  • TG_OP — contiene información en texto sobre la operación actual: INSERT, UPDATE o DELETE.

Todas estas variables están disponibles automáticamente dentro de la función asociada al trigger.

La teoría sin práctica es como SQL sin índices: lento y triste. Así que vamos a verlo con ejemplos prácticos.

Uso de OLD para acceder a los datos antiguos

Imagina que tenemos una tabla students. Y alguien corrige la edad de un estudiante (quizá se le cruzaron los cables y pensó que el estudiante aún tiene 20 años, y no ya 25).

CREATE TABLE students (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    age INT NOT NULL
);

Para rastrear qué cambios ocurrieron, vamos a crear una tabla de logs:

CREATE TABLE student_changes (
    change_id SERIAL PRIMARY KEY,
    student_id INT NOT NULL,
    old_value INT,
    new_value INT,
    change_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Ahora vamos más allá: creamos una función que va a registrar los cambios. Aquí es donde OLD nos viene de perlas:

CREATE OR REPLACE FUNCTION log_student_changes()
RETURNS TRIGGER AS $$
BEGIN
    -- Registramos los cambios de edad
    INSERT INTO student_changes (student_id, old_value, new_value)
    VALUES (OLD.id, OLD.age, NEW.age);
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

Y ahora creamos el trigger:

CREATE TRIGGER student_age_update
AFTER UPDATE OF age ON students
FOR EACH ROW
WHEN (OLD.age IS DISTINCT FROM NEW.age) -- Solo si la edad cambió
EXECUTE FUNCTION log_student_changes();

Vamos a añadir un estudiante y luego modificarlo:

INSERT INTO students (name, age) VALUES ('Alicia', 20);

UPDATE students
SET age = 25
WHERE name = 'Alicia';

-- Revisamos el log de cambios:
SELECT * FROM student_changes;

Vas a ver que en la tabla de logs se registró el cambio: edad de 20 a 25. ¿Magia? No, OLD.

Uso de NEW para los datos nuevos

Ahora imagina que queremos, al añadir un nuevo estudiante, registrar automáticamente su ID y nombre en la tabla de logs (sí, es un poco paranoico, pero a veces útil):

CREATE OR REPLACE FUNCTION log_new_student()
RETURNS TRIGGER AS $$
BEGIN
    -- Registramos los datos del nuevo estudiante
    INSERT INTO student_changes (student_id, old_value, new_value)
    VALUES (NEW.id, NULL, NEW.age); -- No hay valor antiguo, porque es un INSERT
    RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER student_insert_log
AFTER INSERT ON students
FOR EACH ROW
EXECUTE FUNCTION log_new_student();

De nuevo, añadimos un nuevo estudiante y revisamos:

INSERT INTO students (name, age) VALUES ('Bob', 22);

-- Revisamos el log:
SELECT * FROM student_changes;

Verás que en los logs aparece el nuevo estudiante. ¡Eso sí que es cuidar los datos a otro nivel!

Uso de TG_OP para saber el tipo de operación

Pero, ¿y si queremos tener un trigger universal para registrar logs, que pueda manejar INSERT, UPDATE e incluso DELETE? Aquí es donde la variable TG_OP nos salva.

Vamos a crear una función universal:

CREATE OR REPLACE FUNCTION log_all_operations()
RETURNS TRIGGER AS $$
BEGIN
    IF TG_OP = 'INSERT' THEN
        INSERT INTO student_changes (student_id, old_value, new_value)
        VALUES (NEW.id, NULL, NEW.age);

    ELSIF TG_OP = 'UPDATE' THEN
        INSERT INTO student_changes (student_id, old_value, new_value)
        VALUES (OLD.id, OLD.age, NEW.age);

    ELSIF TG_OP = 'DELETE' THEN
        INSERT INTO student_changes (student_id, old_value, new_value)
        VALUES (OLD.id, OLD.age, NULL);

    END IF;

    RETURN NULL; -- Para triggers AFTER en DELETE devolvemos NULL
END;
$$ LANGUAGE plpgsql;

Creamos un trigger que se dispare en las tres operaciones:

CREATE TRIGGER universal_student_log
AFTER INSERT OR UPDATE OR DELETE ON students
FOR EACH ROW
EXECUTE FUNCTION log_all_operations();

Añadimos, modificamos y borramos un estudiante:

INSERT INTO students (name, age) VALUES ('Charlie', 30);
UPDATE students SET age = 31 WHERE name = 'Charlie';
DELETE FROM students WHERE name = 'Charlie';

-- Revisamos el log:
SELECT * FROM student_changes;

Puedes ver el log de todas las operaciones — ¡un trigger para gobernarlos a todos!

Errores típicos al usar OLD, NEW, TG_OP

Al trabajar con triggers, puedes encontrarte con algunos problemas comunes:

"¿Por qué OLD no funciona en un insert?" Es el comportamiento por defecto: para INSERT no hay datos antiguos. Usa NEW.

"¿Qué hago si NEW no funciona en un delete?" De nuevo, es lo esperado: para DELETE no hay datos nuevos. Usa OLD.

La lógica del trigger causa recursión infinita. Asegúrate de que el trigger no se llame a sí mismo por accidente. Para eso puedes poner condiciones claras en el bloque WHEN o comprobar TG_OP.

1
Cuestionario/control
Introducción a los triggers, nivel 57, lección 4
No disponible
Introducción a los triggers
Introducción a los triggers
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION