CodeGym /Cursos /Spring Data JPA /Transacciones en BD: commit y rollback

Transacciones en BD: commit y rollback

Spring Data JPA
Nivel 3, Lección 3
Disponible

1. El problema del «medio pedido»

Si una operación de varios pasos falla en medio, los datos fácilmente quedan atrapados en el estado de «hecho a medias» — ese es el problema del «medio pedido». Imagina mini-shop: en el código todo parece ordenado — insertamos la fila del pedido, insertamos las líneas, descontamos el stock. Pero la base de datos no es telepática y no lee tu «guión con intención» por la expresión de tu cara. Si entre los pasos ocurre un error, al desarrollador rápidamente le dan ganas de mudarse al bosque a cultivar patatas.

Miremos el problema sin filosofía, al nivel de los hechos. Tenemos las tablas (del nivel anterior):

  • customer_order — registro raíz del pedido;
  • order_item — líneas del pedido;
  • stock_item — stock.

La gestión de un pedido debe ser lógicamente atómica: o el pedido está creado y las líneas están creadas y el stock está descontado, o no se ha creado nada. Pero si los comandos se ejecutan uno a uno y cada uno se confirma por separado, puede ocurrir este guión: el pedido y las líneas ya se han escrito, pero el descuento del stock ha fallado — por ejemplo, no había suficiente stock o llegó un error. Resultado: el pedido «existe», pero el stock del almacén «no ha disminuido» o ha disminuido incorrectamente — y la realidad empieza a dispersarse.

Para sentir lo desagradable que es esto, basta con hacerse mentalmente la pregunta: «¿Qué le mostraré al usuario?» Si el pedido ya existe en customer_order, formalmente «la gestión ha pasado». Pero si el stock no se ha descontado, el almacén dice: «Yo no entregué nada». Resulta una disputa entre dos tablas. Y una disputa entre tablas, lamentablemente, siempre la gana la tabla con los datos incorrectos.

2. Modo autocommit

La palabra autocommit suena amigable, casi como «guardado automático» en Google Docs. Y en los casos simples es realmente cómodo: ejecutaste un INSERT — y de inmediato se convierte en un «hecho oficial» en la base. Pero en cuanto la operación de negocio consiste en varios pasos, el autocommit se convierte en una persona que después de cada frase firma el contrato, aunque más adelante en el texto ponga «ojo, esto es un borrador, no firmar».

El sentido del autocommit es muy simple: cada comando SQL se ejecuta en su propia transacción y se confirma automáticamente. Es decir:

  • enviaste INSERT — la base ejecutó y de inmediato «confirmó»;
  • enviaste el segundo INSERT — la base ejecutó y volvió a confirmar;
  • enviaste UPDATE — y aquí ocurrió un error… pero los comandos anteriores ya están confirmados, no hay marcha atrás.

En términos de JDBC (no nos adentramos en el curso de JDBC, simplemente fijamos el dato) la Connection por defecto a menudo tiene autoCommit = true. En muchos clientes SQL también hay una casilla Auto-commit, y normalmente está marcada. En psql los comandos también van como transacciones separadas hasta que haces explícitamente BEGIN.

Aquí una «peligrosa» secuencia mínima (pseudocódigo) que parece una operación de negocio, pero técnicamente son tres confirmaciones independientes:

// Activamos el autocommit: cada comando se convierte en una transacción separada.
connection.setAutoCommit(true);

execute("insert into customer_order ..."); // ya confirmado (no se puede revertir)
execute("insert into order_item ...");     // ya confirmado (no se puede revertir)
execute("update stock_item ...");          // aquí el error — ya es tarde

Desde el punto de vista de la base esto no es «una sola operación de gestión del pedido». Son simplemente tres comandos y tres microvidas separadas. Y si la tercera vida terminó en tragedia, las dos primeras continúan existiendo como si nada hubiera pasado.

Para que quede totalmente claro, aquí hay una pequeña tabla:

Modo de ejecución Qué pasa después de cada comando ¿Adecuado para una operación de varios pasos?
autocommit = true el comando se confirma automáticamente normalmente no
autocommit = false +
commit()
al final
solo se confirma el escenario completo sí

3. Transacción explícita: BEGIN/COMMIT/ROLLBACK

Ahora la buena noticia: las bases de datos inventaron las transacciones mucho antes que Spring, Hibernate y todas nuestras anotaciones favoritas. Y la transacción a nivel de BD es exactamente el mecanismo que permite decir: «Estos varios comandos son un bloque semántico único. O todo, o nada». El punto más importante aquí es que nosotros mismos marcamos los límites de ese bloque.

En PostgreSQL la forma clásica tiene este aspecto: BEGIN (o START TRANSACTION) — luego los comandos — luego COMMIT o ROLLBACK.

Mini-ejemplo en SQL puro (muy corto, para no convertir la lección en un escenario separado del pedido):

begin; -- iniciamos la transacción: los cambios siguientes son por ahora «borrador»

insert into customer_order (id, order_number, customer_email, total_amount)
values (700, 'ORD-700', 'alex@example.com', 399.98); -- paso 1: creamos el pedido

update stock_item
set available_quantity = available_quantity - 2
where product_id = 10; -- paso 2: descontamos el stock

commit; -- confirmamos ambos pasos como un todo

Y si algo salió mal (un error o la decisión de la aplicación de «no continuar el escenario»), en lugar de commit haces rollback:

begin; -- iniciamos la transacción

insert into customer_order (id, order_number, customer_email, total_amount)
values (700, 'ORD-700', 'alex@example.com', 399.98); -- alcanzamos a hacer el cambio

-- supongamos que aquí detectamos un problema
rollback; -- cancelamos TODOS los cambios de esta transacción

Es útil tener en mente un diagrama de flujo simple:

flowchart TD
    A[BEGIN] --> B[Ejecutamos los pasos de la operación de negocio]
    B --> C{¿Todos los pasos tuvieron éxito?}
    C -- Sí --> D[COMMIT]
    C -- No --> E[ROLLBACK]

En este esquema no hay nada «del framework». Es la base de datos pura. Los frameworks después simplemente aprenderán a hacer lo mismo automáticamente, pero ahora nos importa entender el fundamento: una transacción es un contenedor para varias acciones SQL.

4. Atomicidad de la operación de negocio

La palabra «atomicidad» suena como si ahora fuera a venir la física cuántica con el gato de Schrödinger en el papel de DBA. En realidad todo es más simple: la atomicidad significa que la operación parece indivisible. O ha ocurrido completamente, o se considera que no ha ocurrido en absoluto. En el mundo de los datos esto significa: después de completarse la transacción no hay en la base ningún estado de «a medias».

La analogía más comprensible es una transferencia bancaria. Si descontaste el dinero de la cuenta A pero no lo abonaste en la cuenta B, el dinero no «flota en el aire» — se pierde, y tú pierdes el sueño. Por eso un sistema normal debe hacer esto como un todo: el descuento y el abono deben estar vinculados por la transacción.

En mini-shop la situación es muy similar. Si creamos el pedido pero no descontamos el stock, le dijimos al sistema: «El pedido existe», pero no aseguramos su realidad en el almacén. Y si al revés — descontamos el stock pero el pedido no se guardó, entonces el producto ha desaparecido del almacén «hacia ningún sitio». Ambos escenarios para el negocio son igualmente malos, solo que el contable llora por razones distintas.

Es importante captar la sensación: la atomicidad no es «por comodidad», sino sobre la supervivencia de los datos. Y precisamente la transacción hace posible la atomicidad en escenarios de varios comandos.

5. COMMIT: confirmación de los cambios

Si BEGIN es «hemos empezado la historia», el COMMIT es «la publicamos como versión oficial». A partir del COMMIT los cambios quedan confirmados: se guardan en la base como parte del estado permanente, y simplemente «cambiar de opinión» con el comando ROLLBACK ya no es posible. Después del commit la cancelación se hace solo mediante una nueva operación de negocio — por ejemplo, la cancelación del pedido o la devolución del stock —, y eso ya es una lógica completamente distinta.

Es muy importante sentir: el COMMIT no es simplemente el toque final técnico. Es el momento con sentido que debe ocurrir cuando la operación de negocio está realmente completada y todos sus pasos están hechos.

Por eso en un escenario normal el COMMIT está:

— no después del primer INSERT, porque eso todavía no es el pedido «como un todo»;
— no después de insertar las líneas, si el stock todavía no se ha descontado;
— sino al final, cuando estamos seguros de que todo ha pasado con éxito.

En términos de comportamiento esto puede formularse así: el commit confirma el conjunto completo de cambios de la transacción de golpe, como un «paquete» único. Eso es la atomicidad práctica.

Si se quiere ver esto en términos concretos, se puede recordar una idea simple: «El COMMIT es el botón "Guardar" para toda la historia, no para un comando».

6. ROLLBACK: reversión de los cambios

En un principiante la palabra rollback a veces genera la sensación de que es un botón de emergencia en una central nuclear. En realidad el ROLLBACK es una parte normal y habitual del escenario. Los errores ocurren, las condiciones no se cumplen, los datos pueden no pasar las restricciones, puede no haber suficiente stock — y en esos casos el comportamiento correcto del sistema es revertir los cambios y dejar la base en el estado inicial, como si no hubiera habido intento.

Hay dos tipos importantes de razones para la reversión. El primero es un error técnico, cuando el comando realmente falla (por ejemplo, una violación de FK o el intento de escribir NULL en un campo NOT NULL). El segundo es una condición de negocio, cuando el comando formalmente podría ejecutarse, pero nosotros mismos decidimos «no continuamos»: por ejemplo, la actualización del stock no afectó a ninguna fila porque no hay suficiente mercancía, y no queremos guardar el pedido.

En PostgreSQL hay una peculiaridad útil que es mejor conocer ahora que a las 2 de la madrugada en producción. Si dentro de la transacción ocurrió un error de ejecución del SQL, la transacción puede pasar al estado «rota», y hasta que no hagas el ROLLBACK, la base responderá en el estilo de: «No, ya no voy a hacer nada, primero reviértete».

Esto puede verse en un ejemplo simple:

begin; -- iniciamos la transacción

insert into customer_order (id, order_number, customer_email, total_amount)
values (1, 'ORD-1', 'a@b.c', 10.00); -- alcanzamos a crear el pedido

-- supongamos que aquí hay un error (por ejemplo, violación de FK)
insert into order_item (id, order_id, product_id, quantity, unit_price, line_total)
values (1, 999, 10, 1, 10.00, 10.00); -- order_id=999 no existe => error

-- después del error algunos comandos pueden no ejecutarse hasta que revertamos
rollback; -- volvemos al «estado antes del begin»

Moraleja: el ROLLBACK no es la «parte vergonzosa del código». Es la conclusión lógica de un intento fallido. En una arquitectura saludable la reversión está prevista de forma tan explícita como el commit exitoso.

7. Límites de la transacción

Después de conocer BEGIN/COMMIT/ROLLBACK, surge la pregunta natural: «¿Y dónde poner los límites de la transacción? ¿En cada comando? ¿En cada tabla? ¿En cada método?». Y aquí es importante no ir a los extremos: la transacción no debe ser «mínima», sino «con sentido».

Si la transacción es demasiado pequeña, deja de resolver el problema de la atomicidad. Por ejemplo, «insertamos el pedido — commit; insertamos las líneas — commit; descontamos el stock — commit» técnicamente parece ordenado, pero la operación de negocio ya se ha descompuesto en partes, y entre ellas son posibles las roturas. Es como montar el mueble de IKEA y después de cada tornillo decir: «Bueno, el armario está listo» — y luego sorprenderse de que las puertas estén en el suelo por separado.

Si la transacción es demasiado grande, se convierte en una bolsa donde se ha metido todo. La base se ve obligada a mantener los recursos más tiempo, y después resulta más difícil entender dónde empezó y dónde terminó la operación. Por ahora no analizamos los aislamientos y los bloqueos en profundidad, pero incluso a nivel básico es útil recordar una idea simple: la transacción no es una infinidad gratuita.

En nuestro mini-shop un buen ejemplo de «tamaño correcto» es la gestión del pedido. Es una sola operación de negocio que incluye varias tablas, y precisamente ella debe ser una sola transacción. En cambio, la lectura de la lista de productos del catálogo normalmente no es una operación de cambio de estado así (hoy no nos adentramos en los detalles, simplemente fijamos la intuición).

Es decir, la regla en términos humanos suena así: el límite de la transacción debe coincidir con el límite del cambio de datos con sentido.

8. Transacciones en Java sin Spring

Todavía estamos en el mundo del SQL, pero es útil ver cómo los «botones» del commit/rollback parecen desde el lado de Java. No para escribir código JDBC a mano durante todo el curso (no lo haremos), sino para entender: al final cualquier herramienta en Java llega de todas formas a estas operaciones básicas.

Aquí hay un ejemplo mínimo a nivel JDBC. Es intencionadamente corto y sin adornos arquitectónicos para que veas solo la mecánica:

import java.sql.Connection;

// Desactivamos el autocommit: ahora controlamos nosotros los límites de la transacción.
connection.setAutoCommit(false);

try {
    // Dentro de la transacción hacemos varios pasos de una operación de negocio.
    execute(connection, "insert into customer_order ...");
    execute(connection, "insert into order_item ...");
    execute(connection, "update stock_item ...");

    // Confirmamos todo de golpe, si todos los pasos tuvieron éxito.
    connection.commit();
} catch (Exception ex) {
    // Si en cualquier paso hay un error — revertimos TODO lo que alcanzamos a hacer.
    connection.rollback();
    throw ex;
}

Aquí lo importante no es la función execute (puede ser PreparedStatement o cualquier cosa), sino la propia estructura: desactivamos el autocommit, ejecutamos una serie de pasos, confirmamos con un comando, y ante el error hacemos la reversión.

Si se quiere ver algo más realista, podemos imaginar que executeUpdate devuelve el número de filas modificadas, y ya en ese nivel tomamos la decisión de negocio. Por ejemplo, si UPDATE stock_item ... actualizó 0 filas, significa que no había suficiente stock, y nosotros mismos iniciamos la reversión (esta es una idea importante para el escenario del pedido):

import java.sql.Connection;

// Pasamos a la gestión manual de la transacción.
connection.setAutoCommit(false);

try {
    // Paso 1: creamos el pedido (o su parte).
    execute(connection, "insert into customer_order ...");

    // Paso 2: intentamos descontar el stock; el resultado — cuántas filas se actualizaron realmente.
    int updated = executeUpdate(connection, "update stock_item ...");

    // Si no actualizamos ninguna fila, consideramos la operación de negocio fallida.
    if (updated == 0) throw new IllegalStateException("Not enough stock");

    // Si todo está bien — confirmamos todo el escenario.
    connection.commit();
} catch (Exception ex) {
    // Cualquier error (tanto técnico como de negocio) => reversión de la transacción.
    connection.rollback();
    throw ex;
}

Observa el efecto psicológico útil: la transacción en el código parece un solo bloque try/catch. Esa es la idea de «unidad de trabajo»: o llegamos al commit(), o revertimos todo.

9. Verificación del ROLLBACK en la práctica

Las transacciones son más fáciles de entender cuando al menos una vez ves con tus propios ojos el efecto de «como si no hubiera pasado nada». Por eso es útil tener en mente una pequeña verificación que puede hacerse en el cliente SQL incluso sin una segunda conexión y sin modos de aislamiento complejos.

El escenario es este: dentro de la transacción insertamos una fila y de inmediato la vemos con una consulta SELECT. Luego hacemos el ROLLBACK y volvemos a ejecutar el SELECT — la fila ya no está.

Ejemplo (condicional pero muy ilustrativo):

begin; -- iniciamos la transacción

insert into category (id, code, name, active)
values (10, 'BOOKS', 'Books', true); -- la inserción es visible por ahora solo «dentro» de la transacción

select count(*) from category where id = 10; -- 1 (dentro de la transacción vemos nuestra inserción)

rollback; -- revertimos la inserción

select count(*) from category where id = 10; -- 0 (después de la reversión es como si no hubiera pasado nada)

Esta experiencia ayuda al cerebro a dejar de percibir la transacción como una cosa abstracta del libro de texto. Ves literalmente: dentro de la transacción los cambios existen, pero todavía no se han convertido en «historia oficial». El COMMIT los convierte en oficiales, el ROLLBACK tira el borrador a la papelera.

Y esto es precisamente lo que necesitamos para las operaciones de varios pasos en mini-shop: hasta que no estemos seguros de que todos los pasos han tenido éxito, trabajamos en modo borrador.

10. Errores típicos con transacciones

Con las transacciones los problemas surgen más a menudo no porque la persona «no sepa el comando COMMIT», sino porque elige incorrectamente los límites y tiene una actitud equivocada hacia los errores. Por eso analicemos los rastrillazos más frecuentes en un estilo vivo, para que los reconozcas por el sonido de los pasos que se acercan.

Error n.º 1: confiar en el autocommit en una operación compuesta.
El principiante ve que el INSERT y el UPDATE «funcionan sin más» y no incluye la transacción, pensando que eso es «para sistemas complejos». Como resultado ante un error en el paso n.º 3 los pasos n.º 1-2 ya están confirmados, y la base almacena una «semi-operación». Se cura con exactamente una solución: en cuanto tienes más de un comando que juntos forman un sentido, empiezas a pensar en términos de transacción.

Error n.º 2: hacer el commit en medio del escenario «por si acaso».
A veces parece lógico «confirmar el pedido y luego ya ver el stock». Esto crea una ilusión de fiabilidad, pero en realidad rompe la atomicidad. Después del primer commit ya no puedes revertir la creación del pedido, aunque todo lo demás haya fallado. El commit correcto — al final, cuando realmente has completado la operación de negocio.

Error n.º 3: olvidarse del rollback en la rama del error o «tragar» la excepción.
En el código esto parece especialmente tramposo: capturaste la excepción, la registraste en el log, continuaste el trabajo, y la transacción quedó abierta o en estado de error. Como resultado los siguientes comandos empiezan a comportarse de forma extraña, y no entiendes por qué. El modelo mental aquí debe ser estricto: si la operación no llegó al commit, estás obligado a hacer el rollback — explícitamente e inmediatamente.

Error n.º 4: creer que el «error de SQL» es la única razón para la reversión.
Muy a menudo la reversión se necesita no por un fallo del comando, sino por una condición de negocio. Por ejemplo, el UPDATE stock_item ... con la condición available_quantity >= 2 puede devolver 0 filas actualizadas — esto no es un error de SQL, sino un resultado correcto con el sentido de «no había suficiente stock». Si en ese caso de todas formas haces el commit, confirmas el pedido sin descuento del stock. Aquí es importante acostumbrarse: la transacción es una herramienta no solo para las emergencias, sino también para la lógica de negocio normal de «no ejecutamos el escenario».

Error n.º 5: mezclar acciones no relacionadas en una sola transacción.
A veces el desarrollador piensa: «Ya que la transacción hace todo fiable, metamos en una transacción y la creación del pedido, y la actualización del producto en el catálogo, y encima el cambio de categoría». El resultado es una transacción-cosechadora, en la que es difícil entender el sentido y que hace el sistema menos predecible. La transacción debe corresponder a una operación, no a todo lo que «resultó estar a mano».

1
Tarea
Spring Data JPA, nivel 3, lección 3
Bloqueada
Inserción de pedido con posterior rollback
Inserción de pedido con posterior rollback
1
Tarea
Spring Data JPA, nivel 3, lección 3
Bloqueada
Transacción exitosa con commit
Transacción exitosa con commit
Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION