BEFORE UPDATE

Un trigger BEFORE UPDATE se ejecuta antes de que una fila existente sea modificada en la tabla. Es el tipo de trigger más utilizado en aplicaciones reales porque permite interceptar los cambios, validar que los nuevos valores cumplen las reglas de negocio, transformar datos automáticamente y, si es necesario, cancelar la operación antes de que llegue a escribirse en disco.

Dentro de un trigger BEFORE UPDATE tienes acceso a dos pseudoregistros fundamentales: OLD, que contiene los valores actuales de la fila antes de la modificación, y NEW, que contiene los valores que se van a guardar. La diferencia clave respecto a los triggers AFTER UPDATE es que en un BEFORE UPDATE puedes modificar los valores de NEW para ajustarlos antes de que se persistan, algo que no es posible en un trigger AFTER. Además, puedes cancelar completamente la operación lanzando un error con SIGNAL.

Esta capacidad convierte a los triggers BEFORE UPDATE en una herramienta esencial para mantener la integridad de los datos más allá de lo que permiten las restricciones declarativas como CHECK, FOREIGN KEY o UNIQUE. Mientras que esas restricciones validan condiciones simples, un trigger puede implementar lógica de negocio arbitrariamente compleja: limitar el porcentaje de descuento según la categoría del producto, impedir transiciones de estado inválidas en un flujo de trabajo, o registrar automáticamente la fecha y el usuario de la última modificación.

Sintaxis

La sintaxis para crear un trigger BEFORE UPDATE sigue el patrón general de creación de triggers en MySQL. Es necesario usar DELIMITER para cambiar el delimitador de sentencia, ya que el cuerpo del trigger contiene puntos y coma internos que de otro modo MySQL interpretaría como fin de la sentencia.

DELIMITER //
 
CREATE TRIGGER nombre_trigger
BEFORE UPDATE ON nombre_tabla
FOR EACH ROW
BEGIN
    -- OLD.columna = valor actual (solo lectura)
    -- NEW.columna = valor nuevo (modificable)
END //
 
DELIMITER ;

La cláusula FOR EACH ROW indica que el trigger se ejecuta una vez por cada fila afectada por la sentencia UPDATE. Si una sentencia actualiza 500 filas, el trigger se ejecutará 500 veces. Esto es importante tenerlo en cuenta desde el punto de vista del rendimiento, especialmente si el cuerpo del trigger contiene lógica pesada.

El nombre del trigger debe ser único dentro de la base de datos. Una convención habitual es usar el prefijo tr_ seguido del nombre de la tabla y el tipo de evento, como tr_productos_before_update. Esto facilita identificar rápidamente qué hace cada trigger cuando consultas la lista con SHOW TRIGGERS.

Validar cambios de precio

El caso de uso más clásico de un trigger BEFORE UPDATE es validar que los nuevos valores cumplen ciertas restricciones de negocio. En este ejemplo, creamos un trigger sobre la tabla productos que impide precios negativos, limita las reducciones de precio a un máximo del 50 % y corrige automáticamente valores de stock negativos.

DELIMITER //
 
CREATE TRIGGER tr_productos_before_update
BEFORE UPDATE ON productos
FOR EACH ROW
BEGIN
    -- No permitir precio negativo
    IF NEW.precio < 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'El precio no puede ser negativo';
    END IF;
 
    -- No permitir reducción de precio mayor al 50%
    IF NEW.precio < OLD.precio * 0.5 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'No se puede reducir el precio más del 50%';
    END IF;
 
    -- No permitir stock negativo: corregir automáticamente
    IF NEW.stock < 0 THEN
        SET NEW.stock = 0;
    END IF;
END //
 
DELIMITER ;

Observa la diferencia entre las dos estrategias de manejo: para el precio, el trigger rechaza la operación con SIGNAL porque un precio inválido es un error que el usuario debe corregir. Para el stock negativo, el trigger corrige silenciosamente el valor a 0 porque la lógica de negocio permite ese ajuste automático. Elegir entre rechazar y corregir depende del contexto de cada aplicación.

Veamos cómo se comporta este trigger en la práctica:

-- Esto funciona: reducción del 10%
UPDATE productos SET precio = 1169.99 WHERE id = 1;
 
-- Esto falla: reducción mayor al 50%
-- UPDATE productos SET precio = 100 WHERE id = 1;
-- Error: No se puede reducir el precio más del 50%

La primera sentencia se ejecuta sin problemas porque 1169.99 es más del 50 % del precio original. La segunda sentencia lanzaría un error porque intenta reducir el precio a un valor inferior al 50 % del actual. El UPDATE completo se cancela y la fila no se modifica.

Registrar fecha y usuario de modificación

Otro uso muy frecuente de los triggers BEFORE UPDATE es actualizar automáticamente campos de auditoría como la fecha de última modificación y el usuario que realizó el cambio. Esto garantiza que estos campos se mantengan siempre actualizados sin depender de que la aplicación los incluya en cada sentencia UPDATE.

DELIMITER //
 
CREATE TRIGGER tr_clientes_before_update
BEFORE UPDATE ON clientes
FOR EACH ROW
BEGIN
    SET NEW.fecha_modificacion = NOW();
    SET NEW.modificado_por = CURRENT_USER();
END //
 
DELIMITER ;

Con este trigger activo, cualquier UPDATE sobre la tabla clientes registrará automáticamente cuándo y quién realizó el cambio. Incluso si la aplicación olvida enviar estos campos, el trigger se encarga de rellenarlos. Esto es especialmente valioso en entornos donde múltiples aplicaciones o scripts acceden a la misma base de datos.

UPDATE clientes SET telefono = '612345678' WHERE id = 15;
 
SELECT nombre, telefono, fecha_modificacion, modificado_por
FROM clientes WHERE id = 15;
nombretelefonofecha_modificacionmodificado_por
Laura Sánchez6123456782026-03-24 14:32:07app_user@localhost

Prevenir cambios en campos protegidos

En muchos sistemas hay campos que nunca deberían cambiar después de la creación del registro. Por ejemplo, el cliente asociado a un pedido no debería modificarse una vez creado, y un pedido cancelado no debería poder reactivarse. Un trigger BEFORE UPDATE puede implementar estas reglas de integridad que van más allá de las restricciones estándar de SQL.

DELIMITER //
 
CREATE TRIGGER tr_pedidos_before_update
BEFORE UPDATE ON pedidos
FOR EACH ROW
BEGIN
    -- No permitir cambiar el cliente de un pedido
    IF OLD.cliente_id != NEW.cliente_id THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'No se puede cambiar el cliente de un pedido existente';
    END IF;
 
    -- No permitir reactivar pedidos cancelados
    IF OLD.estado = 'cancelado' AND NEW.estado != 'cancelado' THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'No se puede reactivar un pedido cancelado';
    END IF;
 
    -- Validar transiciones de estado válidas
    IF OLD.estado = 'entregado' AND NEW.estado = 'pendiente' THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'No se puede volver a estado pendiente un pedido entregado';
    END IF;
END //
 
DELIMITER ;

Este trigger implementa una máquina de estados simple para los pedidos. Las transiciones permitidas fluyen de forma lógica: un pedido puede pasar de "pendiente" a "enviado", de "enviado" a "entregado", y desde cualquier estado a "cancelado", pero no puede retroceder. Este tipo de validación es extremadamente difícil de garantizar sin un trigger, ya que dependería de que todas las aplicaciones cliente implementen la misma lógica.

Normalizar datos antes de guardar

Los triggers BEFORE UPDATE son ideales para normalizar o limpiar datos antes de que se almacenen. Esto asegura consistencia en la base de datos independientemente de cómo lleguen los datos desde la aplicación.

DELIMITER //
 
CREATE TRIGGER tr_clientes_normalizar
BEFORE UPDATE ON clientes
FOR EACH ROW
BEGIN
    -- Normalizar email a minúsculas
    SET NEW.email = LOWER(TRIM(NEW.email));
 
    -- Capitalizar nombre: primera letra en mayúscula
    SET NEW.nombre = CONCAT(
        UPPER(LEFT(NEW.nombre, 1)),
        LOWER(SUBSTRING(NEW.nombre, 2))
    );
 
    -- Eliminar espacios extra del teléfono
    SET NEW.telefono = REPLACE(REPLACE(NEW.telefono, ' ', ''), '-', '');
END //
 
DELIMITER ;

Con este trigger, no importa si el usuario escribe el email como "MARIA@GMAIL.COM" o " maria@gmail.com ": siempre se almacenará en formato normalizado. Lo mismo ocurre con el nombre y el teléfono. Esta normalización centralizada es mucho más fiable que depender de la validación del lado del cliente, ya que protege la base de datos de cualquier punto de entrada.

UPDATE clientes
SET nombre = 'PEDRO', email = '  PEDRO@OUTLOOK.ES  ', telefono = '612-345-678'
WHERE id = 7;
 
SELECT nombre, email, telefono FROM clientes WHERE id = 7;
nombreemailtelefono
Pedropedro@outlook.es612345678

Comparar OLD y NEW para auditoría selectiva

A veces no quieres registrar todas las actualizaciones, sino solo las que modifican campos específicos. Comparando OLD y NEW puedes detectar exactamente qué cambió y actuar en consecuencia. Este patrón es común en sistemas que mantienen un historial de cambios para cumplimiento normativo o para funcionalidades de "deshacer".

DELIMITER //
 
CREATE TRIGGER tr_productos_historial_precio
BEFORE UPDATE ON productos
FOR EACH ROW
BEGIN
    -- Solo registrar si el precio realmente cambió
    IF OLD.precio != NEW.precio THEN
        INSERT INTO historial_precios (
            producto_id, precio_anterior, precio_nuevo,
            fecha_cambio, usuario
        ) VALUES (
            OLD.id, OLD.precio, NEW.precio,
            NOW(), CURRENT_USER()
        );
    END IF;
 
    -- Solo registrar si el stock cambió significativamente (más del 20%)
    IF OLD.stock > 0 AND ABS(NEW.stock - OLD.stock) > OLD.stock * 0.2 THEN
        INSERT INTO alertas_inventario (
            producto_id, stock_anterior, stock_nuevo,
            fecha_alerta, tipo
        ) VALUES (
            OLD.id, OLD.stock, NEW.stock,
            NOW(),
            CASE WHEN NEW.stock < OLD.stock THEN 'reduccion' ELSE 'incremento' END
        );
    END IF;
END //
 
DELIMITER ;

La comparación OLD.precio != NEW.precio evita insertar registros duplicados en el historial cuando se actualiza un producto pero el precio no cambia. Esto mantiene la tabla de historial limpia y con información relevante.

Errores comunes

El error más frecuente al trabajar con triggers BEFORE UPDATE es intentar modificar los valores de OLD. Los valores de OLD son de solo lectura y representan el estado actual de la fila antes de la modificación. Solo los valores de NEW pueden modificarse dentro de un trigger BEFORE.

Otro error habitual es crear triggers con lógica demasiado pesada que ralentiza las operaciones de actualización. Recuerda que el trigger se ejecuta una vez por cada fila afectada. Si un UPDATE modifica 10,000 filas y el trigger hace una consulta adicional por cada una, el rendimiento se degradará considerablemente. Intenta mantener la lógica del trigger lo más ligera posible y evita consultas a otras tablas cuando sea factible.

También es importante saber que solo puede existir un trigger BEFORE UPDATE por tabla en MySQL. Si necesitas ejecutar múltiples acciones, debes combinarlas todas en un solo trigger. Esto a veces resulta en triggers largos, pero es una limitación del motor que debes tener en cuenta al diseñar tu esquema.

Finalmente, ten cuidado con las dependencias circulares. Si un trigger BEFORE UPDATE en la tabla A actualiza la tabla B, y un trigger en la tabla B actualiza la tabla A, puedes generar un bucle infinito. MySQL detecta este tipo de recursión y lanza un error, pero es mejor evitarlo en el diseño.

Cuándo usar BEFORE UPDATE

Utiliza un trigger BEFORE UPDATE cuando necesites validar o transformar datos antes de que se almacenen y esta lógica no pueda expresarse con restricciones declarativas como CHECK o FOREIGN KEY. Los escenarios más comunes incluyen: validación de reglas de negocio complejas, normalización automática de datos, actualización de campos de auditoría, protección de campos inmutables y control de transiciones de estado.

Si solo necesitas reaccionar a los cambios sin modificarlos (por ejemplo, enviar una notificación o actualizar una tabla de resumen), un trigger AFTER UPDATE es más apropiado. La regla general es: usa BEFORE cuando necesites modificar o rechazar los nuevos valores, y AFTER cuando necesites actuar como consecuencia de un cambio ya confirmado.

Para ver todos los triggers activos en tu base de datos, usa SHOW TRIGGERS. Y si necesitas eliminar un trigger que ya no es necesario, consulta DROP TRIGGER.

Limpieza

Para eliminar los triggers creados en los ejemplos de este artículo, ejecuta las siguientes sentencias:

DROP TRIGGER IF EXISTS tr_productos_before_update;
DROP TRIGGER IF EXISTS tr_pedidos_before_update;
DROP TRIGGER IF EXISTS tr_clientes_before_update;
DROP TRIGGER IF EXISTS tr_clientes_normalizar;
DROP TRIGGER IF EXISTS tr_productos_historial_precio;

En el siguiente artículo veremos los triggers AFTER UPDATE.

Escrito por Eduardo Lázaro