Handlers · SIGNAL · SQLSTATE · Transacciones
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.
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.
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 |
|---|---|---|
| SQLEXCEPTION | Cualquier error SQL --- la condición más amplia. | Tabla inexistente, FK violada, valor duplicado. |
| SQLWARNING | Advertencias: algo salió raro pero no es un error fatal. | Intentar crear una tabla que ya existe. |
| NOT FOUND | Una consulta SELECT...INTO no devolvió filas. | Buscar un socio con un ID que no está en la tabla. |
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:
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
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 HANDLER | EXIT HANDLER | |
|---|---|---|
| ¿Qué hace? | Ejecuta el handler y sigue | Ejecuta el handler y sale del bloque |
| ¿Sigue el SP? | Sí, desde la instrucción siguiente | No, el bloque termina |
| ¿Cuándo usarlo? | Errores no críticos: loguear y continuar | Errores críticos donde continuar sería peligroso |
| Ejemplo típico | Registrar un ítem fallido y seguir con el siguiente | Transferencia bancaria donde un fallo no puede continuar |
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).
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.
-- 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
-- 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.
-- 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? | Ventaja | Desventaja |
|---|---|---|---|
| A | Mensaje genérico alcanza | Simple, poco código | No se sabe exactamente qué falló |
| B | Mensajes específicos por cada validación | Todo en un SP, claro y explícito | El SP puede volverse extenso |
| C | Validaciones reutilizables en varios SPs | Código principal limpio, reutilizable | Se deben crear y mantener varios SPs |
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ódigo | Nombre del error | ¿Cuándo ocurre? |
|---|---|---|
| 1146 | Table doesn't exist | INSERT/SELECT sobre una tabla que no existe. |
| 1050 | Table already exists | CREATE TABLE con el nombre de una tabla que ya existe. |
| 1062 | Duplicate entry | INSERT de un valor ya existente en una columna UNIQUE o PK. |
| 1048 | Column cannot be null | INSERT/UPDATE con NULL en una columna definida como NOT NULL. |
| 1365 | Division by zero | División por cero en una expresión aritmética. |
| 1452 | Foreign key violation | INSERT/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
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).
-- 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
-- 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_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_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
| HANDLER | SIGNAL | |
|---|---|---|
| ¿Qué hace? | Captura errores que ocurren | Lanza errores que el programador genera |
| ¿Quién lo usa? | Para reaccionar ante fallos de MySQL | Para validar reglas de negocio propias |
| Analogía | try / catch en otros lenguajes | throw / raise en otros lenguajes |
| SQLSTATE | La condición que se desea atrapar | El código que se asigna al error propio |
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.
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:
El primer handler que coincide es el que se ejecuta. Los handlers exteriores no intervienen si ya se encontró uno en un nivel interior.
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.
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.
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.
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.
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.
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.
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).
Cada procedimiento maneja sus propios errores. Cuando un procedimiento hijo llama a otro:
-- 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
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ística | InnoDB | MyISAM |
|---|---|---|
| Transacciones (COMMIT/ROLLBACK) | Sí, soporte completo | No --- cada operación es inmediata e irreversible |
| Claves foráneas (FOREIGN KEY) | Sí, con control automático | Las ignora sin error |
| Bloqueo al escribir | Por fila --- más eficiente | Por tabla entera --- bloquea todo |
| Recuperación ante crashes | Automática con redo log | Puede requerir reparación manual |
| Motor por defecto desde | MySQL 5.5 (2010) hasta hoy | Era default en versiones antiguas |
-- 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;
Una transacción es un grupo de operaciones que se trata como una unidad indivisible: o todas se confirman, o ninguna.
| Propiedad | Qué garantiza | Ejemplo |
|---|---|---|
| Atomicidad | Todo o nada. Si falla cualquier parte, se revierte todo. | Si falla el segundo UPDATE de una transferencia, el primero también se deshace. |
| Consistencia | La 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. |
| Aislamiento | Las transacciones concurrentes no se ven entre sí. | Dos transferencias simultáneas no se pisan. |
| Durabilidad | El COMMIT es permanente --- sobrevive reinicios y caídas. | Una vez confirmado, el cambio no se pierde ni si el servidor cae. |
| Comando | ¿Qué hace? | ¿Cuándo usarlo? |
|---|---|---|
| START TRANSACTION | Abre la transacción. Los cambios desde aquí son temporales. | Al inicio de cualquier bloque de operaciones que deben ser atómicas. |
| COMMIT | Confirma todos los cambios. Los hace permanentes y visibles. | Cuando todas las operaciones salieron bien. |
| ROLLBACK | Descarta todos los cambios desde el START TRANSACTION. | Cuando algo falló y se desea volver al estado anterior. |
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
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.
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
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
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.
-- 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.
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.
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: