Imagina que estás programando una app para una tienda online, y durante el pago de un pedido necesitas:
- Retener el dinero de la tarjeta del cliente.
- Reducir la cantidad del producto en el almacén.
- Crear un registro de la transacción exitosa.
¿Qué pasa si algo sale mal en medio de estas acciones? Por ejemplo, el producto se agota en el almacén después de que el dinero ya fue retenido, pero antes de crear el registro del pedido. Todo se va al garete: el dinero queda "en el limbo", el pedido no se completa y tu servidor recibe toneladas de correos enfadados (y quizás alguna demanda).
Las transacciones justo ayudan a evitar estas situaciones. Te permiten agrupar varias operaciones en una sola unidad "atómica" de trabajo con la base de datos. Es como el botón "Deshacer" en un editor de texto: si algo sale mal, simplemente vuelves al principio.
¿Cómo las transacciones aseguran la integridad de los datos?
Las transacciones se basan en el concepto ACID:
- Atomicidad (Atomicity) — Todas las operaciones dentro de la transacción se ejecutan completamente o no se ejecuta nada. "Todo o nada".
- Consistencia (Consistency) — Los datos permanecen en un estado consistente antes y después de la transacción.
- Aislamiento (Isolation) — Una transacción no interfiere con otras.
- Durabilidad (Durability) — Cuando la transacción termina, su resultado se guarda incluso si el sistema falla.
¿Por qué lo repito otra vez? Porque es el ideal al que todos aspiran. Y... que rara vez se alcanza. Cuando volvamos a ver transacciones más adelante en el curso, verás que a veces hay que sacrificar algunos principios ACID.
Así que disfruta mientras las transacciones son tan simples y bonitas. ¡Vamos ya a los ejemplos!
Ejemplo de uso de transacciones
Vamos a ver el escenario de añadir un estudiante y registrarlo en un curso.
Supón que trabajamos con la base de datos de una universidad. Ahora tenemos oyentes externos en nuestros cursos. Si hay plaza en el curso, registramos a ese oyente como estudiante (temporalmente) y lo añadimos al curso. Así es como va el proceso.
Al añadir un nuevo estudiante a la base y registrarlo en un curso necesitamos:
- Añadir un registro en la tabla
students. - Crear un registro en la tabla
enrollments, que conecta al estudiante con el curso.
Si algo falla (por ejemplo, el curso ya está lleno), tenemos que deshacer la operación para que los datos no se desincronicen entre las tablas. Así se hace:
-- Inicio de la transacción
BEGIN;
-- Paso 1: Añadimos al estudiante
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Masculino')
RETURNING id;
-- Supón que devuelve id = 10
-- Paso 2: Lo registramos en el curso
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 5);
-- ¿Todo fue bien? Guardamos los cambios
COMMIT;
¿Qué pasa si ocurre un error?
De repente ocurre un error al registrar en el curso: por ejemplo, el curso no existe. Si te olvidas de la transacción, el registro del estudiante en la tabla students se queda, pero en la tabla enrollments no. Eso rompe la integridad de los datos. Para evitarlo, podemos usar el comando ROLLBACK.
-- Inicio de la transacción
BEGIN;
-- Paso 1: Añadimos al estudiante
INSERT INTO students (name, age, gender)
VALUES ('Otto Lin', 20, 'Masculino')
RETURNING id;
-- Paso 2: Intentamos registrarlo en el curso
INSERT INTO enrollments (student_id, course_id)
VALUES (10, 999); -- Error: ¡no existe el curso con id = 999!
-- Deshacemos todos los cambios
ROLLBACK;
Como resultado, ninguna de las operaciones se ejecuta y la base de datos queda igual que antes de la transacción.
Uso de SAVEPOINT para el control
Ahora imagina un escenario más complicado. Quieres hacer varias operaciones, pero en algún punto necesitas volver solo hasta un punto concreto, no cancelar todo el proceso.
Vamos a implementar un registro paso a paso del estudiante
-- Inicio de la transacción
BEGIN;
-- Añadimos al estudiante
SAVEPOINT add_student; -- Creamos un punto de guardado
INSERT INTO students (name, age, gender)
VALUES ('Anna Song', 22, 'Femenino');
-- La registramos en el primer curso
SAVEPOINT enroll_course_1; -- Otro punto de guardado
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 5);
-- La registramos en el segundo curso (aquí hay error)
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 999); -- ¡Error!
-- Volvemos solo al último punto de guardado
ROLLBACK TO enroll_course_1;
-- Continuamos el proceso
INSERT INTO enrollments (student_id, course_id)
VALUES (11, 6);
-- Guardamos los cambios
COMMIT;
Así, los errores en una parte del proceso no afectan a los datos de otras partes.
Comprobando si hubo cambios
Si una consulta SQL cambia algo, puedes comprobar si realmente hubo algún cambio o no.
Puedes tener una situación donde haces un DELETE, pero ninguna fila cumple el WHERE. O haces un UPDATE, pero los datos ya estaban cambiados y en realidad no pasó nada.
Para esto existe la variable de sistema especial FOUND. Indica si se tocaron filas en la última consulta SQL:
FOUND = TRUE— la consulta actualizó/eliminó algo;FOUND = FALSE— no se eliminó ni cambió nada.
Con un SELECT normal no funciona, solo para rastrear cambios.
Aplicación práctica: procesamiento de pagos
Las transacciones son especialmente útiles en apps financieras. Vamos otra vez con un sistema que debe transferir dinero de una cuenta a otra.
-- Inicio de la transacción
BEGIN;
-- Paso 1: Quitamos dinero de la primera cuenta
UPDATE accounts
SET balance = balance - 100
WHERE id = 1 AND balance >= 100;
-- Paso 2: Comprobamos que la operación fue exitosa (se cambiaron filas)
IF NOT FOUND THEN
ROLLBACK; -- Deshacer si no hay suficiente dinero
RAISE EXCEPTION '¡Fondos insuficientes!'; -- ¡Error! Lanzamos excepción
END IF;
-- Paso 3: Añadimos dinero a la segunda cuenta
UPDATE accounts
SET balance = balance + 100
WHERE id = 2;
-- Guardamos la transacción
COMMIT;
Aquí, si el cliente intenta transferir más dinero del que tiene en su cuenta, la transacción se deshace y la base de datos no queda en un estado "colgado".
Características y errores típicos
Olvidar COMMIT: si al final de la transacción olvidas hacer COMMIT, la base de datos se queda "esperando" y los cambios no se guardan.
Olvidar WHERE: actualizar o borrar datos sin condición puede tener consecuencias catastróficas. Por ejemplo, el comando DELETE FROM students sin WHERE borra a todos los estudiantes.
Transacciones largas: si una transacción está abierta demasiado tiempo, puede bloquear el acceso a los datos y causar problemas de rendimiento. Siempre termina las transacciones (COMMIT o ROLLBACK) lo antes posible.
Las transacciones son tu mejor amigo cuando se trata de asegurar la integridad de los datos. Ayudan a evitar inconsistencias, sobre todo en escenarios complejos como registro de usuarios, procesamiento de pagos o actualización de tablas relacionadas. Si dominas los comandos BEGIN, COMMIT, ROLLBACK y SAVEPOINT, podrás crear apps mucho más robustas y seguras.
GO TO FULL VERSION