BASES DE DATOS II

Stored Procedures Avanzados

Handlers · SIGNAL · SQLSTATE · Transacciones

Prof: Lic. Adrian Aroca

UTN Mar del Plata
TUP --- 2do Año | Plan 2024


Este material es continuación del repaso de SPs del año anterior.
El punto de partida son los handlers --- todo lo que sigue es tema nuevo.

01 ¿Por qué necesitamos manejar errores?

Hasta ahora los procedimientos almacenados que se escribían asumían que todo iba a salir bien: el socio existía, el plan estaba en la tabla, los valores eran válidos. Pero en una aplicación real las cosas fallan.

¿Qué hace MySQL cuando ocurre un error dentro de un SP sin ningún handler declarado?

-- sin_handler.sql --- sin manejo de errores
CREATE PROCEDURE inscribirSocio(IN p_id_socio INT, IN p_id_plan INT)
BEGIN
    INSERT INTO actividades (id_socio, id_plan, fecha)
    VALUES (p_id_socio, p_id_plan, NOW());
    SELECT 'Inscripción registrada' AS resultado;
END

Si p_id_socio no existe en la tabla socios (violación de FK), MySQL lanza el error 1452, el SP se interrumpe en ese punto y el SELECT final nunca se ejecuta. La aplicación recibe un mensaje técnico sin contexto útil.

! El problema real
No es que MySQL lance un error --- eso está bien. El problema es que el SP queda interrumpido a mitad de camino, potencialmente dejando datos inconsistentes. Los handlers son la solución: se le dice a MySQL qué hacer cuando algo falla, en lugar de dejar que explote.

02 Los tres tipos de condición de error

Antes de ver cómo se declara un handler, es necesario entender qué condiciones se pueden manejar. MySQL define tres condiciones generales:

Condición¿Cuándo se activa?Ejemplo típico
SQLEXCEPTIONCualquier error SQL --- la condición más amplia.Tabla inexistente, FK violada, valor duplicado.
SQLWARNINGAdvertencias: algo salió raro pero no es un error fatal.Intentar crear una tabla que ya existe.
NOT FOUNDUna consulta SELECT...INTO no devolvió filas.Buscar un socio con un ID que no está en la tabla.
→ Importante sobre NOT FOUND
El handler NOT FOUND solo se activa con SELECT...INTO. Si se usa SELECT * FROM socios WHERE id = 99 y no hay resultados, MySQL devuelve un resultado vacío pero NO activa el handler --- simplemente no hay filas. Para activarlo, es necesario asignar el resultado a una variable con INTO.

03 CONTINUE vs EXIT --- ¿qué pasa después del error?

Una vez definida la condición a manejar, se debe decidir qué hace el SP después de que se activa el handler. Hay dos comportamientos posibles:

CONTINUE HANDLER

Ejecuta el bloque del handler y luego continúa con la instrucción siguiente al error. El SP no se interrumpe.

-- continue_handler.sql
CREATE PROCEDURE ejemploContinue()
BEGIN
    DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
    BEGIN
        SELECT 'Ocurrió un error, pero el SP sigue ejecutando' AS aviso;
    END;

    INSERT INTO tabla_que_no_existe VALUES ('x');
    SELECT 'El SP llegó hasta acá igual' AS confirmacion;
END

EXIT HANDLER

Ejecuta el bloque del handler y luego sale del BEGIN...END actual. Las instrucciones siguientes al error no se ejecutan.

-- exit_handler.sql
CREATE PROCEDURE ejemploExit()
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SELECT 'Error crítico: el SP se detuvo aquí' AS aviso;
    END;

    INSERT INTO tabla_que_no_existe VALUES ('x');
    SELECT 'Esto no se verá jamás' AS mensaje;
END
CONTINUE HANDLEREXIT HANDLER
¿Qué hace?Ejecuta el handler y sigueEjecuta el handler y sale del bloque
¿Sigue el SP?Sí, desde la instrucción siguienteNo, el bloque termina
¿Cuándo usarlo?Errores no críticos: loguear y continuarErrores críticos donde continuar sería peligroso
Ejemplo típicoRegistrar un ítem fallido y seguir con el siguienteTransferencia bancaria donde un fallo no puede continuar
✓ Regla práctica
En la gran mayoría de los casos se utilizará EXIT HANDLER. Si algo falla en medio de una inserción o una transacción, lo más seguro es detener la ejecución y devolver un mensaje claro. CONTINUE se reserva para flujos donde el error es esperado y no compromete la integridad de los datos.

04 Primer ejemplo completo con NOT FOUND

Ahora que se conocen las condiciones y los comportamientos, se puede armar un ejemplo real. El objetivo es verificar si un socio existe antes de operar con él:

-- buscarSocio.sql
CREATE PROCEDURE buscarSocio(IN p_id_socio INT)
BEGIN
    DECLARE v_id INT;
    DECLARE EXIT HANDLER FOR NOT FOUND
    BEGIN
        SELECT 'El socio no existe en la tabla' AS msj_error;
    END;

    SELECT id_socio INTO v_id FROM socios WHERE id_socio = p_id_socio;
    SELECT 'Socio encontrado' AS resultado;
END

El flujo es el siguiente: si el id existe en la tabla, v_id recibe el valor, el handler nunca se activa y se muestra 'Socio encontrado'. Si el id no existe, SELECT...INTO no devuelve filas, se activa el handler NOT FOUND y se muestra el mensaje de error. Luego el SP termina (EXIT).

05 Manejar múltiples errores en un mismo SP

El caso anterior tenía un único punto de fallo. ¿Qué sucede cuando se necesitan verificar dos o más datos? Por ejemplo, que tanto el socio como el plan existan antes de insertar una actividad.

Opción A --- Un único handler (mensaje genérico)

-- buscarSocioYPlan_simple.sql
CREATE PROCEDURE buscarSocioYPlan(IN p_id_socio INT, IN p_id_plan INT)
BEGIN
    DECLARE v_id INT;
    DECLARE EXIT HANDLER FOR NOT FOUND
    BEGIN
        SELECT 'Uno o ambos IDs buscados no se encontraron' AS msj_error;
    END;

    SELECT id_socio INTO v_id FROM socios WHERE id_socio = p_id_socio;
    SELECT id_plan INTO v_id FROM planes WHERE id_plan = p_id_plan;
    SELECT 'Socio y plan encontrados' AS resultado;
END
! Limitación de este enfoque
Si falla la búsqueda del socio o la del plan, ambas muestran el mismo mensaje. No es posible saber cuál fue la que falló. Esto está bien cuando el mensaje genérico es suficiente, pero si se necesita más detalle, se debe encapsular.

Opción B --- Encapsulamiento con bloques BEGIN...END anidados

-- buscarSocioYPlan_encapsulado.sql
CREATE PROCEDURE buscarSocioYPlan(IN p_id_socio INT, IN p_id_plan INT)
BEGIN
    DECLARE v_id INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SELECT 'Ocurrió un error inesperado. Proceso terminado.' AS error_general;
    END;

    BEGIN
        DECLARE EXIT HANDLER FOR NOT FOUND
        BEGIN
            SELECT 'El ID de socio no existe' AS msj_error;
        END;
        SELECT id_socio INTO v_id FROM socios WHERE id_socio = p_id_socio;
        SELECT 'Socio encontrado';
    END;

    BEGIN
        DECLARE EXIT HANDLER FOR NOT FOUND
        BEGIN
            SELECT 'El ID de plan no existe' AS msj_error;
        END;
        SELECT id_plan INTO v_id FROM planes WHERE id_plan = p_id_plan;
        SELECT 'Plan encontrado';
    END;
END

Ahora si falla la búsqueda del socio, se muestra 'El ID de socio no existe'. Si falla la del plan, se muestra 'El ID de plan no existe'. El handler general del bloque exterior atrapa cualquier otro error que no sea NOT FOUND.

→ Regla de anidamiento
Un mismo BEGIN...END no puede tener dos handlers para la misma condición --- MySQL lo rechaza al compilar. La solución es justamente lo que se hizo: bloques anidados, cada uno con su propio handler. El error activa el handler del bloque más interno donde ocurrió.

Opción C --- Modularización con procedimientos independientes

-- buscarSocioYPlan_modular.sql
CREATE PROCEDURE verificarSocio(IN p_id_socio INT)
BEGIN
    DECLARE v_id INT;
    DECLARE EXIT HANDLER FOR NOT FOUND
    BEGIN
        SELECT 'El ID de socio no existe' AS msj_error;
    END;
    SELECT id_socio INTO v_id FROM socios WHERE id_socio = p_id_socio;
END;

CREATE PROCEDURE verificarPlan(IN p_id_plan INT)
BEGIN
    DECLARE v_id INT;
    DECLARE EXIT HANDLER FOR NOT FOUND
    BEGIN
        SELECT 'El ID de plan no existe' AS msj_error;
    END;
    SELECT id_plan INTO v_id FROM planes WHERE id_plan = p_id_plan;
END;

CREATE PROCEDURE buscarSocioYPlan(IN p_id_socio INT, IN p_id_plan INT)
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SELECT 'Error inesperado en el proceso.' AS error_general;
    END;
    CALL verificarSocio(p_id_socio);
    CALL verificarPlan(p_id_plan);
    SELECT 'Socio y plan verificados correctamente' AS resultado;
END
Opción¿Cuándo usarla?VentajaDesventaja
AMensaje genérico alcanzaSimple, poco códigoNo se sabe exactamente qué falló
BMensajes específicos por cada validaciónTodo en un SP, claro y explícitoEl SP puede volverse extenso
CValidaciones reutilizables en varios SPsCódigo principal limpio, reutilizableSe deben crear y mantener varios SPs

06 Handlers por código de error numérico

Además de las condiciones generales, MySQL asigna un número específico a cada tipo de error. Esto permite reaccionar de forma diferente según el error exacto que ocurrió.

-- handler_codigo_numerico.sql
DECLARE EXIT HANDLER FOR 1146  -- Tabla inexistente
BEGIN
    SELECT 'La tabla especificada no existe en la base de datos' AS msj_error;
END;

Los errores más comunes que se encuentran en el trabajo con SPs son:

CódigoNombre del error¿Cuándo ocurre?
1146Table doesn't existINSERT/SELECT sobre una tabla que no existe.
1050Table already existsCREATE TABLE con el nombre de una tabla que ya existe.
1062Duplicate entryINSERT de un valor ya existente en una columna UNIQUE o PK.
1048Column cannot be nullINSERT/UPDATE con NULL en una columna definida como NOT NULL.
1365Division by zeroDivisión por cero en una expresión aritmética.
1452Foreign key violationINSERT/UPDATE con un valor que no existe en la tabla referenciada.
-- handlers_multiples_codigos.sql
CREATE PROCEDURE insertarActividad(IN p_id_socio INT, IN p_id_plan INT)
BEGIN
    DECLARE EXIT HANDLER FOR 1452
    BEGIN
        SELECT 'Error: el socio o el plan especificado no existe' AS msj_error;
    END;
    DECLARE EXIT HANDLER FOR 1062
    BEGIN
        SELECT 'Error: ya existe una actividad con esos datos' AS msj_error;
    END;
    INSERT INTO actividades (id_socio, id_plan, fecha)
    VALUES (p_id_socio, p_id_plan, NOW());
    SELECT 'Actividad registrada correctamente' AS resultado;
END
→ Prioridad de handlers
Cuando ocurre un error, MySQL activa el handler más específico disponible. Si se tiene declarado un handler para el error 1062 y también uno para SQLEXCEPTION, y ocurre un error 1062, se activa el de 1062. SQLEXCEPTION actúa como red de seguridad general para todo lo que no tenga handler específico.
! ¿Qué errores no tienen código?
Los errores de lógica de negocio --- por ejemplo, que un precio sea negativo o que una fecha de inicio sea posterior a la de fin --- NO tienen código de error en MySQL porque son reglas propias, no del motor. Para esos casos existe SIGNAL.

07 SIGNAL --- lanzar errores propios

SIGNAL permite lanzar errores desde dentro de un SP, con el mensaje y el código que se definan. Es equivalente al throw de otros lenguajes de programación.

-- signal_sintaxis.sql
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'mensaje descriptivo del error';

El código estándar para errores definidos por el usuario es '45000'. Siempre conviene usarlo para errores propios (no de MySQL).

Ejemplo: validar que el precio de un plan sea positivo

-- signal_precio.sql
CREATE PROCEDURE actualizarPrecio(IN p_id_plan INT, IN p_precio DECIMAL(10,2))
BEGIN
    IF p_precio <= 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'El precio del plan debe ser mayor a cero';
    END IF;
    UPDATE planes SET precio = p_precio WHERE id_plan = p_id_plan;
    SELECT 'Precio actualizado correctamente' AS resultado;
END

Reutilizar el handler con una variable de mensaje

-- signal_reutilizar_handler.sql
CREATE PROCEDURE registrarPago(IN p_id_socio INT, IN p_monto DECIMAL(10,2), IN p_cuotas INT)
BEGIN
    DECLARE v_msj_error VARCHAR(200);
    DECLARE EXIT HANDLER FOR SQLSTATE '45000'
    BEGIN
        SELECT v_msj_error AS msj_error;
    END;

    IF p_monto <= 0 THEN
        SET v_msj_error = 'El monto del pago debe ser mayor a cero';
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msj_error;
    END IF;

    IF p_cuotas < 1 OR p_cuotas > 12 THEN
        SET v_msj_error = 'La cantidad de cuotas debe estar entre 1 y 12';
        SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = v_msj_error;
    END IF;

    INSERT INTO pagos (id_socio, monto, cuotas, fecha)
    VALUES (p_id_socio, p_monto, p_cuotas, NOW());
    SELECT 'Pago registrado correctamente' AS resultado;
END

SIGNAL dentro de un handler --- capturar y relanzar

-- signal_en_handler.sql
CREATE PROCEDURE insertarSocio(IN p_dni VARCHAR(20), IN p_nombre VARCHAR(100))
BEGIN
    DECLARE CONTINUE HANDLER FOR 1062
    BEGIN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'Ya existe un socio registrado con ese DNI';
    END;
    INSERT INTO socios (dni, nombre) VALUES (p_dni, p_nombre);
    SELECT 'Socio registrado correctamente' AS resultado;
END

RESIGNAL --- relanzar un error capturado

-- resignal_basico.sql
CREATE PROCEDURE ejemplo_resignal()
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SELECT 'Error capturado, se relanza' AS aviso;
        RESIGNAL;   -- El error continúa hacia el exterior
    END;
    SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Error de prueba';
    SELECT 'Esta línea no se ejecuta' AS mensaje;
END
HANDLERSIGNAL
¿Qué hace?Captura errores que ocurrenLanza errores que el programador genera
¿Quién lo usa?Para reaccionar ante fallos de MySQLPara validar reglas de negocio propias
Analogíatry / catch en otros lenguajesthrow / raise en otros lenguajes
SQLSTATELa condición que se desea atraparEl código que se asigna al error propio

08 Comportamiento de handlers: reglas y jerarquía

Para diseñar correctamente el manejo de errores en procedimientos almacenados, es fundamental comprender cómo MySQL resuelve qué handler se ejecuta y con qué consecuencias. A continuación se presentan las reglas esenciales (máximas) que rigen el comportamiento de los handlers.

8.1 Regla de búsqueda (el más interno gana)

Cuando ocurre un error, MySQL busca el handler más interno que coincida con la condición. La búsqueda se realiza en el siguiente orden:

  1. Bloque actual (el BEGIN...END más profundo donde ocurrió el error).
  2. Bloque padre (si existe).
  3. Así sucesivamente hasta el nivel del procedimiento.

El primer handler que coincide es el que se ejecuta. Los handlers exteriores no intervienen si ya se encontró uno en un nivel interior.

8.2 Regla de alcance (scope)

Un handler solo afecta al bloque en el que está declarado. Los handlers definidos en un bloque interno no tienen efecto fuera de él, y los handlers del nivel procedimiento solo actúan si ningún bloque interno los ha capturado antes.

8.3 Regla de EXIT

Un handler de tipo EXIT termina el bloque donde está declarado. Si el handler está en un bloque interno, solo sale de ese bloque; la ejecución continúa después del END de dicho bloque. Si el handler está a nivel del procedimiento, termina todo el procedimiento.

8.4 Regla de CONTINUE

Un handler de tipo CONTINUE ejecuta su código y luego continúa con la instrucción inmediatamente siguiente a aquella que generó el error. No corta el flujo del bloque. Es especialmente útil para cursores y para manejar advertencias.

8.5 Regla del error no manejado

Si ocurre un error y no existe ningún handler que lo capture (ya sea en el bloque actual o en niveles superiores), el procedimiento completo se aborta de inmediato. No se ejecuta ninguna instrucción posterior, sin importar en qué bloque se originó el error.

8.6 Regla de SIGNAL

SIGNAL se comporta exactamente igual que cualquier excepción lanzada por MySQL. Sigue el mismo proceso de búsqueda de handlers. Si no se encuentra un handler que lo capture, el procedimiento se aborta por completo.

8.7 Regla de RESIGNAL

RESIGNAL se utiliza dentro de un handler para relanzar el error que se acaba de capturar. Permite que el error “suba” al nivel superior, donde puede ser manejado por otro handler o provocar la terminación del SP si no existe.

8.8 Regla de consumo del error

Si un handler captura un error y no ejecuta RESIGNAL, el error se considera “consumido”. No se propaga a niveles superiores. La ejecución continúa de acuerdo con el tipo de handler (EXIT o CONTINUE).

8.9 Regla entre procedimientos (padre/hijo)

Cada procedimiento maneja sus propios errores. Cuando un procedimiento hijo llama a otro:

8.10 Procedimiento para analizar cualquier caso

  1. Localizar el error: ¿En qué bloque BEGIN...END ocurre?
  2. Buscar handler: ¿Existe un handler para esa condición en ese bloque? Si no, subir al bloque padre.
  3. Tipo de handler: ¿Es EXIT o CONTINUE?
  4. ¿Hay RESIGNAL? Si el handler incluye RESIGNAL, el error sigue propagándose.
  5. Si nadie captura: El SP se aborta completamente.

Ejemplo de comunicación entre procedimientos (sin propagación de error)

-- comunicacion_padre_hijo.sql
CREATE PROCEDURE hijo(OUT p_ok BOOLEAN, OUT p_mensaje VARCHAR(100))
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        SET p_ok = FALSE;
        SET p_mensaje = 'Error en el procedimiento hijo';
    END;
    -- Operación que puede fallar
    SIGNAL SQLSTATE '45000';
    SET p_ok = TRUE;
    SET p_mensaje = 'Proceso exitoso';
END;

CREATE PROCEDURE padre()
BEGIN
    DECLARE v_ok BOOLEAN;
    DECLARE v_mensaje VARCHAR(100);
    CALL hijo(v_ok, v_mensaje);
    IF v_ok THEN
        SELECT 'Padre: todo bien' AS resultado;
    ELSE
        SELECT CONCAT('Padre: error detectado: ', v_mensaje) AS resultado;
    END IF;
    SELECT 'Padre continúa' AS continuacion;
END
Resumen de reglas clave
- El handler más interno que coincide es el que se ejecuta.
- EXIT termina el bloque donde fue declarado; si es el bloque principal, termina todo el SP.
- CONTINUE no interrumpe el flujo del bloque.
- Un error sin handler aborta todo el SP.
- SIGNAL lanza errores que se comportan como los de MySQL.
- RESIGNAL relanza un error capturado.
- Si un handler no hace RESIGNAL, el error queda consumido y no afecta a niveles superiores.
- En llamadas entre procedimientos, el manejo es independiente: el padre solo recibe el error si el hijo no lo captura o lo relanza.

09 Motores de almacenamiento: InnoDB y MyISAM

Antes de abordar las transacciones es necesario entender qué es un motor de almacenamiento. Es el componente interno de MySQL que decide cómo se guardan, leen y gestionan los datos en el disco. No cambia cómo se escribe SQL, pero sí qué funcionalidades están disponibles.

CaracterísticaInnoDBMyISAM
Transacciones (COMMIT/ROLLBACK)Sí, soporte completoNo --- cada operación es inmediata e irreversible
Claves foráneas (FOREIGN KEY)Sí, con control automáticoLas ignora sin error
Bloqueo al escribirPor fila --- más eficientePor tabla entera --- bloquea todo
Recuperación ante crashesAutomática con redo logPuede requerir reparación manual
Motor por defecto desdeMySQL 5.5 (2010) hasta hoyEra default en versiones antiguas
! Importante
Si una tabla usa MyISAM, los comandos START TRANSACTION, COMMIT y ROLLBACK se ejecutan sin ningún efecto: cada INSERT, UPDATE o DELETE se confirma inmediatamente y de forma permanente. No hay forma de deshacerlos. Para usar transacciones, la tabla DEBE ser InnoDB.
-- motor_innodb.sql
-- Ver el motor actual de una tabla
SHOW TABLE STATUS WHERE Name = 'socios';
-- Especificar motor al crear una tabla
CREATE TABLE pagos (
    id INT AUTO_INCREMENT PRIMARY KEY,
    id_socio INT,
    monto DECIMAL(10,2)
) ENGINE = InnoDB;
-- Cambiar el motor de una tabla existente
ALTER TABLE pagos ENGINE = InnoDB;
✓ Consejo práctico
En la práctica, siempre se debe usar InnoDB. Es el motor por defecto desde MySQL 5.5, el más estable, y el único que soporta transacciones y foreign keys. No hay razón para usar MyISAM en un proyecto nuevo.

10 Transacciones y ACID

Una transacción es un grupo de operaciones que se trata como una unidad indivisible: o todas se confirman, o ninguna.

Las propiedades ACID

PropiedadQué garantizaEjemplo
AtomicidadTodo o nada. Si falla cualquier parte, se revierte todo.Si falla el segundo UPDATE de una transferencia, el primero también se deshace.
ConsistenciaLa BD pasa de un estado válido a otro estado válido.El saldo total entre todas las cuentas se mantiene igual antes y después.
AislamientoLas transacciones concurrentes no se ven entre sí.Dos transferencias simultáneas no se pisan.
DurabilidadEl COMMIT es permanente --- sobrevive reinicios y caídas.Una vez confirmado, el cambio no se pierde ni si el servidor cae.

Los tres comandos de una transacción

Comando¿Qué hace?¿Cuándo usarlo?
START TRANSACTIONAbre la transacción. Los cambios desde aquí son temporales.Al inicio de cualquier bloque de operaciones que deben ser atómicas.
COMMITConfirma todos los cambios. Los hace permanentes y visibles.Cuando todas las operaciones salieron bien.
ROLLBACKDescarta todos los cambios desde el START TRANSACTION.Cuando algo falló y se desea volver al estado anterior.

Ejemplos prácticos de transacciones

Ejemplo 1: Transacción simple con ROLLBACK condicional

En este ejemplo se actualizan dos tablas y si alguna condición no se cumple, se revierte todo.

-- transaccion_condicional.sql
CREATE PROCEDURE actualizarStock(IN p_producto_id INT, IN p_cantidad INT, IN p_precio_unitario DECIMAL(10,2))
BEGIN
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Error: operación cancelada' AS resultado;
    END;

    START TRANSACTION;
    UPDATE productos SET stock = stock - p_cantidad WHERE id = p_producto_id;
    INSERT INTO ventas (producto_id, cantidad, precio_unitario, fecha)
    VALUES (p_producto_id, p_cantidad, p_precio_unitario, NOW());

    IF (SELECT stock FROM productos WHERE id = p_producto_id) < 0 THEN
        ROLLBACK;
        SELECT 'Stock insuficiente, transacción cancelada' AS resultado;
    ELSE
        COMMIT;
        SELECT 'Venta registrada correctamente' AS resultado;
    END IF;
END

Ejemplo 2: Uso de SAVEPOINT para deshacer parte de una transacción

Los puntos de guardado permiten realizar rollback parcial sin perder todo el trabajo de la transacción.

-- savepoint_simple.sql
CREATE PROCEDURE procesar_pedido()
BEGIN
    START TRANSACTION;
    INSERT INTO pedidos (cliente_id, fecha) VALUES (1, NOW());
    SET @pedido_id = LAST_INSERT_ID();

    SAVEPOINT antes_detalles;

    INSERT INTO detalles_pedido (pedido_id, producto, cantidad)
    VALUES (@pedido_id, 'Producto A', 5);

    -- Si ocurre algún problema con los detalles, se revierte solo esa parte
    IF @error_detalle = 1 THEN
        ROLLBACK TO SAVEPOINT antes_detalles;
        SELECT 'Error en detalles, se cancela solo esa parte' AS aviso;
    END IF;

    COMMIT;  -- Confirma el pedido (con o sin detalles según el caso)
END

Nota: SAVEPOINT crea un punto intermedio dentro de la transacción. Con ROLLBACK TO SAVEPOINT nombre se deshacen solo las operaciones posteriores a ese punto, manteniendo lo anterior. Luego se puede continuar con más operaciones y finalmente hacer COMMIT o ROLLBACK completo.

Ejemplo 3: ROLLBACK fuera de un handler

Es importante destacar que ROLLBACK es una instrucción SQL que puede usarse en cualquier lugar del procedimiento, no únicamente dentro de un handler. Se puede ejecutar condicionalmente según la lógica de negocio.

-- rollback_sin_handler.sql
CREATE PROCEDURE actualizar_saldo()
BEGIN
    START TRANSACTION;
    UPDATE cuentas SET saldo = saldo - 100 WHERE id = 1;
    UPDATE cuentas SET saldo = saldo + 100 WHERE id = 2;

    -- Validación de negocio: si el saldo de la cuenta origen quedó negativo
    IF (SELECT saldo FROM cuentas WHERE id = 1) < 0 THEN
        ROLLBACK;  -- ROLLBACK fuera de cualquier handler
        SELECT 'Operación cancelada por saldo negativo' AS resultado;
    ELSE
        COMMIT;
        SELECT 'Transferencia exitosa' AS resultado;
    END IF;
END

Ejemplo 4: Múltiples transacciones en un mismo SP

Es perfectamente posible tener más de un bloque START TRANSACTION / COMMIT / ROLLBACK dentro de un mismo procedimiento. Cada transacción se maneja de forma independiente.

-- multiples_transacciones.sql
CREATE PROCEDURE proceso_multiple()
BEGIN
    -- Primera transacción: actualiza clientes
    START TRANSACTION;
    UPDATE clientes SET estado = 'activo' WHERE id = 1;
    UPDATE clientes SET estado = 'activo' WHERE id = 2;
    COMMIT;  -- Se confirma la primera

    -- Segunda transacción: inserta un log (independiente de la primera)
    START TRANSACTION;
    INSERT INTO logs (mensaje, fecha) VALUES ('Proceso ejecutado', NOW());
    -- Si falla esta inserción, no afecta a la primera transacción
    COMMIT;
END
→ Handlers y transacciones --- el combo ideal
Los handlers y las transacciones se complementan perfectamente: el handler detecta el error y el ROLLBACK deshace los cambios. Sin handler, si algo falla a mitad de la transacción, los cambios parciales quedan ahí. Sin ROLLBACK en el handler, se atrapa el error pero los datos quedan inconsistentes.

11 Caso integrador --- Registrar socio con plan

Se combinan todos los conceptos vistos: parámetros IN, variables locales, handler con ROLLBACK y transacción completa. El escenario es registrar un nuevo socio e inscribirlo en un plan en una sola operación. Las dos inserciones deben ocurrir juntas o no ocurrir.

! El riesgo sin transacción
Sin transacción: si el INSERT en socios tiene éxito pero el INSERT en actividades falla (por ejemplo, porque el id_plan no existe), el socio queda registrado en la tabla pero sin plan asignado. La base queda en estado inconsistente.
-- registrarSocioConPlan.sql
CREATE PROCEDURE registrarSocioConPlan(
    IN p_nombre VARCHAR(100),
    IN p_apellido VARCHAR(100),
    IN p_dni VARCHAR(20),
    IN p_id_plan INT
)
BEGIN
    DECLARE v_id_socio INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Error: el registro fue cancelado y revertido' AS resultado;
    END;

    START TRANSACTION;
    INSERT INTO socios (nombre, apellido, dni)
    VALUES (p_nombre, p_apellido, p_dni);
    SET v_id_socio = LAST_INSERT_ID();
    INSERT INTO actividades (id_socio, id_plan, fecha_inicio)
    VALUES (v_id_socio, p_id_plan, CURDATE());
    COMMIT;
    SELECT CONCAT('Socio registrado con ID ', v_id_socio, ' e inscripto en el plan ', p_id_plan) AS resultado;
END

¿Qué hace cada parte?
- DECLARE v_id_socio: variable local para guardar el ID generado por el INSERT del socio.
- EXIT HANDLER FOR SQLEXCEPTION: si cualquier instrucción falla, hace ROLLBACK y muestra el mensaje de error.
- LAST_INSERT_ID(): devuelve el último ID generado por AUTO_INCREMENT.
- COMMIT: confirma las dos inserciones si todo fue exitoso.

12 Caso integrador --- Transferencia bancaria segura

Este segundo caso agrega validación con SIGNAL antes de iniciar la transacción, y el uso de LEAVE para salir limpiamente del SP cuando la validación falla. El escenario es transferir dinero de una cuenta a otra.

-- transferencia_bancaria.sql
DELIMITER //
CREATE PROCEDURE transferirDinero(
    IN p_origen INT,
    IN p_destino INT,
    IN p_monto DECIMAL(10,2)
)
proc_bloque: BEGIN
    DECLARE v_saldo DECIMAL(10,2);
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SELECT 'Error inesperado. Transferencia cancelada.' AS resultado;
    END;

    IF p_monto <= 0 THEN
        SIGNAL SQLSTATE '45000'
        SET MESSAGE_TEXT = 'El monto de la transferencia debe ser mayor a cero';
    END IF;

    START TRANSACTION;
    SELECT saldo INTO v_saldo FROM cuentas WHERE id = p_origen;
    IF v_saldo < p_monto THEN
        ROLLBACK;
        SELECT 'Saldo insuficiente. Transferencia cancelada.' AS resultado;
        LEAVE proc_bloque;
    END IF;

    UPDATE cuentas SET saldo = saldo - p_monto WHERE id = p_origen;
    UPDATE cuentas SET saldo = saldo + p_monto WHERE id = p_destino;
    COMMIT;
    SELECT 'Transferencia realizada correctamente.' AS resultado;
END //
DELIMITER ;

Conceptos nuevos en este ejemplo
- Etiqueta en BEGIN (proc_bloque: BEGIN): permite referenciar el bloque con LEAVE.
- LEAVE proc_bloque: sale del bloque etiquetado, equivalente a un return.
- SIGNAL antes del START TRANSACTION: valida el monto antes de iniciar la transacción, evitando operaciones innecesarias.

Resumen final

En este documento se ha avanzado desde los conceptos más básicos del manejo de errores hasta la combinación de técnicas que se emplean en aplicaciones reales:

  1. Los tres tipos de condición: SQLEXCEPTION, SQLWARNING y NOT FOUND.
  2. CONTINUE vs EXIT: cuándo detenerse y cuándo seguir.
  3. Handlers para un único error y para múltiples errores en el mismo SP.
  4. Encapsulamiento con BEGIN...END anidados para mensajes específicos.
  5. Modularización con procedimientos auxiliares reutilizables.
  6. Códigos de error numéricos para reaccionar a errores específicos de MySQL.
  7. SIGNAL para lanzar errores propios con mensajes personalizados.
  8. Reglas de jerarquía y comportamiento de handlers (máximas) que permiten modelar correctamente el flujo de errores.
  9. Motores InnoDB y MyISAM: por qué InnoDB es imprescindible para transacciones.
  10. Transacciones con START TRANSACTION, COMMIT, ROLLBACK y SAVEPOINT --- las propiedades ACID con ejemplos prácticos.
  11. Dos casos integradores que combinan handlers, SIGNAL y transacciones.
✓ Para el estudio en casa
Si algún concepto no queda claro, la mejor forma de entenderlo es probarlo en Workbench. Crear los procedimientos de los ejemplos, provocar errores intencionalmente (pasar IDs que no existen, montos negativos, etc.) y observar qué responde MySQL en cada caso. La práctica es lo que fija estos conceptos.