SAVEPOINT
Un SAVEPOINT crea un punto de restauración dentro de una transacción. Permite revertir parte de los cambios sin cancelar toda la transacción.
Sintaxis
-- Crear un savepoint
SAVEPOINT nombre_savepoint;
-- Revertir hasta un savepoint
ROLLBACK TO SAVEPOINT nombre_savepoint;
-- o simplemente
ROLLBACK TO nombre_savepoint;
-- Eliminar un savepoint sin revertir
RELEASE SAVEPOINT nombre_savepoint;Ejemplo básico
START TRANSACTION;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto A', 100.00, 10, 1);
SAVEPOINT sp_a;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto B', 200.00, 5, 1);
SAVEPOINT sp_b;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Producto C', 300.00, 3, 1);
-- Revertir solo Producto C
ROLLBACK TO sp_b;
-- Revertir también Producto B
ROLLBACK TO sp_a;
COMMIT;
-- Solo Producto A se guardóSELECT nombre FROM productos WHERE nombre IN ('Producto A', 'Producto B', 'Producto C');| nombre |
|---|
| Producto A |
-- Limpieza
DELETE FROM productos WHERE nombre = 'Producto A';Savepoints anidados
Puedes crear múltiples savepoints y revertir a cualquiera de ellos:
START TRANSACTION;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 1', 10.00, 1, 1);
SAVEPOINT sp1;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 2', 20.00, 1, 1);
SAVEPOINT sp2;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 3', 30.00, 1, 1);
SAVEPOINT sp3;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Nivel 4', 40.00, 1, 1);
-- Revertir hasta sp2 (elimina Nivel 3 y Nivel 4)
ROLLBACK TO sp2;
-- sp3 ya no existe después del rollback
-- sp1 y sp2 siguen disponibles
COMMIT;
-- Se guardaron Nivel 1 y Nivel 2SELECT nombre FROM productos WHERE nombre LIKE 'Nivel%';| nombre |
|---|
| Nivel 1 |
| Nivel 2 |
DELETE FROM productos WHERE nombre LIKE 'Nivel%';RELEASE SAVEPOINT
Elimina un savepoint sin revertir los cambios. Es útil para liberar recursos cuando ya no necesitas un punto de restauración.
START TRANSACTION;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Release A', 10.00, 1, 1);
SAVEPOINT sp_release;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Release B', 20.00, 1, 1);
-- Ya no necesitamos el savepoint
RELEASE SAVEPOINT sp_release;
-- ROLLBACK TO sp_release ya no funciona
-- Pero ROLLBACK completo sí funciona
COMMIT;DELETE FROM productos WHERE nombre LIKE 'Release%';Caso práctico: pedido con múltiples productos
DELIMITER //
CREATE PROCEDURE sp_pedido_completo(
IN p_cliente_id INT,
OUT p_resultado VARCHAR(200)
)
BEGIN
DECLARE v_pedido_id INT;
DECLARE v_stock INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SET p_resultado = 'Error: pedido cancelado completamente';
END;
START TRANSACTION;
-- Crear pedido
INSERT INTO pedidos (cliente_id, fecha, estado, total)
VALUES (p_cliente_id, NOW(), 'pendiente', 0);
SET v_pedido_id = LAST_INSERT_ID();
SAVEPOINT sp_detalle;
-- Intentar agregar producto 1
SELECT stock INTO v_stock FROM productos WHERE id = 1 FOR UPDATE;
IF v_stock >= 1 THEN
INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (v_pedido_id, 1, 1, (SELECT precio FROM productos WHERE id = 1));
UPDATE productos SET stock = stock - 1 WHERE id = 1;
END IF;
SAVEPOINT sp_detalle2;
-- Intentar agregar producto 2
SELECT stock INTO v_stock FROM productos WHERE id = 2 FOR UPDATE;
IF v_stock >= 1 THEN
INSERT INTO detalle_pedidos (pedido_id, producto_id, cantidad, precio_unitario)
VALUES (v_pedido_id, 2, 1, (SELECT precio FROM productos WHERE id = 2));
UPDATE productos SET stock = stock - 1 WHERE id = 2;
ELSE
-- Si no hay stock del producto 2, revertir solo este detalle
ROLLBACK TO sp_detalle2;
END IF;
-- Actualizar total del pedido
UPDATE pedidos
SET total = (SELECT COALESCE(SUM(cantidad * precio_unitario), 0)
FROM detalle_pedidos WHERE pedido_id = v_pedido_id)
WHERE id = v_pedido_id;
-- Verificar que el pedido tiene al menos un producto
IF (SELECT COUNT(*) FROM detalle_pedidos WHERE pedido_id = v_pedido_id) = 0 THEN
ROLLBACK;
SET p_resultado = 'Error: ningún producto disponible';
ELSE
COMMIT;
SET p_resultado = CONCAT('Pedido ', v_pedido_id, ' creado exitosamente');
END IF;
END //
DELIMITER ;Caso práctico: importación con tolerancia a errores
DELIMITER //
CREATE PROCEDURE sp_importar_productos()
BEGIN
DECLARE v_errores INT DEFAULT 0;
DECLARE v_insertados INT DEFAULT 0;
START TRANSACTION;
-- Producto 1
SAVEPOINT sp_prod;
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
SET v_errores = v_errores + 1;
ROLLBACK TO sp_prod;
END;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Import 1', 100.00, 10, 1);
SET v_insertados = v_insertados + 1;
END;
-- Producto 2 (con error intencional: categoría inválida)
SAVEPOINT sp_prod;
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
SET v_errores = v_errores + 1;
ROLLBACK TO sp_prod;
END;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Import 2', 200.00, 5, 9999);
SET v_insertados = v_insertados + 1;
END;
-- Producto 3
SAVEPOINT sp_prod;
BEGIN
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
SET v_errores = v_errores + 1;
ROLLBACK TO sp_prod;
END;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('Import 3', 300.00, 8, 1);
SET v_insertados = v_insertados + 1;
END;
COMMIT;
SELECT v_insertados AS insertados, v_errores AS errores;
END //
DELIMITER ;Comportamiento importante
| Acción | Efecto sobre savepoints |
|---|---|
ROLLBACK TO sp | Revierte cambios hasta sp, elimina savepoints posteriores a sp |
RELEASE SAVEPOINT sp | Elimina sp sin revertir cambios |
COMMIT | Confirma todo y elimina todos los savepoints |
ROLLBACK | Revierte todo y elimina todos los savepoints |
Savepoints con el mismo nombre
Si creas un savepoint con un nombre que ya existe, el anterior se reemplaza:
START TRANSACTION;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 1', 10.00, 1, 1);
SAVEPOINT sp_reuse;
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 2', 20.00, 1, 1);
SAVEPOINT sp_reuse; -- Reemplaza el anterior
INSERT INTO productos (nombre, precio, stock, categoria_id)
VALUES ('SP Test 3', 30.00, 1, 1);
-- Revierte solo SP Test 3 (hasta el segundo sp_reuse)
ROLLBACK TO sp_reuse;
COMMIT;
-- Se guardaron SP Test 1 y SP Test 2DELETE FROM productos WHERE nombre LIKE 'SP Test%';Limpieza
DROP PROCEDURE IF EXISTS sp_pedido_completo;
DROP PROCEDURE IF EXISTS sp_importar_productos;
DELETE FROM productos WHERE nombre LIKE 'Import%';En el siguiente artículo veremos los niveles de aislamiento de las transacciones.
Escrito por Eduardo Lázaro
