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, a diferencia de un ROLLBACK completo que deshace todo.
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. Para manejar errores automáticamente dentro de una transacción, combina los savepoints con handlers de error.
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
