¿Qué es una transacción?
En SQL, las transacciones son una característica soportada por casi todas las bases de datos serias que te permite ejecutar de forma atómica varias sentencias SQL. Atómico quiere decir que, aunque sean varias sentencias SQL, se van a comportar como un bloque: o se ejecutan todas, o no se ejecuta ninguna.
Por ejemplo, si ejecutas varios INSERTs en una misma transacción, la base de datos hace todas las validaciones pertinentes y reserva el acceso a la tabla igualmente con cada INSERT, pero no aplica los cambios y los persiste hasta que termina de ejecutarse la última sentencia de la transacción. En ese momento es cuando finalmente aplica de golpe todos los cambios en todas las tablas que hayan participado en la transacción. Esto tiene dos consecuencias:
- Si se produce un error durante la inserción y hay que abortar la transacción, como no se insertan las filas realmente en las tablas hasta el último momento, todas las instrucciones se desharán de forma limpia, por lo que no habrá filas a medio meter en ninguna tabla.
- Además, en una base de datos concurrente pueden producirse consultas en paralelo. Si llegase una consulta que hace un SELECT sobre una tabla que ahora mismo tiene pendiente de aplicar una fila como parte de una transacción, esa consulta no verá las nuevas filas ni los nuevos cambios hasta que no se termine la transacción. Es posible cambiar este comportamiento para que la consulta se bloquee y se espere a que se resuelva la transacción, pero lo importante es que mientras no termine la transacción, no estarán los nuevos datos disponibles para otras queries.
Aun así, pese a esto último, dentro de la transacción esos datos sí que estarán disponibles. Es decir, si haces INSERT de un registro e inmediatamente después haces SELECT en la misma transacción, sí que obtendrás las filas insertadas. Esto es importante en casos en los que la inserción de una fila requiera efectos colaterales con la información metida, como utilizar el ID de la fila recién insertada.
Esto es un resumen bastante simplificado que hago por si todavía no sabes cómo funciona una transacción, pero no te libra de leerte bibliografía real sobre el funcionamiento de transacciones en una base de datos, o al menos el artículo de Wikipedia.
Transacciones en JDBC
Por defecto, el driver de JDBC estará configurado para que cuando se hace un execute(), un executeUpdate() o un executeQuery(), este se haga directamente. Si vienes leyendo los capítulos de este curso lo habrás visto hasta ahora, porque no has tenido que pensar todavía en transacciones.
Pero podemos cambiar este comportamiento si configuramos el modo autocommit en una conexión:
try (Connection conn = DriverManager.getConnection(url, props)) {
// Desactivamos el modo auto commit
conn.setAutoCommit(false);
// Operar con la base de datos ahora.
} catch (SQLException e) {
e.printStackTrace();
}
A partir de este momento, si hacemos una sentencia SQL, esa sentencia se asociará con una transacción. Si es la primera, creará la transacción, pero si hay ya una creada porque estás ejecutando varias llamadas a execute(), entonces todas se asociarán en la misma transacción.
Cuando queramos aplicar una transacción, tendremos que hacer commit. El commit es el medio que usamos en una base de datos para terminar la transacción y aplicar todas las sentencias que se han puesto hasta ese momento. El commit lo hacemos con el método commit() de la conexión:
try (Connection conn = DriverManager.getConnection(url, props)) {
// Desactivamos el modo auto commit
conn.setAutoCommit(false);
// Operar con la base de datos ahora.
// Enviar la transacción.
conn.commit();
} catch (SQLException e) {
e.printStackTrace();
}
Rollbacks
Cuando queramos revertir una transacción, podemos usar rollback. Al revertirla, la invalidamos, la descartamos y hacemos como que nunca ha pasado nada. Todas las modificaciones propuestas desde que empezó esa transacción serán ignoradas y no se aplicará ninguna en base de datos la próxima vez que se haga commit.
En el caso de JDBC, puedes hacer un rollback llamando al método rollback() de la clase Connection. Un caso de uso típico de esto sería dejarla en el handler de una excepción para que si se ha producido un fallo al insertar registros, se pueda anular toda la operación:
try (Connection conn = DriverManager.getConnection(url, props)) {
conn.setAutoCommit(false);
insertarRegistros(conn);
conn.commit();
} catch (SQLException e) {
conn.rollback(); // <-- Presta atención
}
Ten en cuenta que cuando se produce un error de forma natural en medio de una transacción, tendrás que llamar a rollback manualmente para desbloquearla. Por ejemplo, si haces una inserción, y se verifica un error de integridad (como una columna cuyo valor es NOT NULL en la que has puesto NULL como valor), se producirá un error que hará fallar cualquier otra sentencia SQL que venga después en la misma transacción. Tendrás que cancelarla con rollback() y empezar una limpia para poder proceder.
Checkpoints
Los checkpoints son otra característica que tienen las transacciones en SQL, que te permite crear un punto de guardado intermedio en mitad de una transacción. Piensa que las transacciones en SQL tienen la siguiente forma:
BEGIN TRANSACTION
-- sentencia 1
-- sentencia 2
-- sentencia 3
-- sentencia 4
-- sentencia 5
COMMIT
Si salen mal, puedes hacer un ROLLBACK:
BEGIN TRANSACTION
-- sentencia 1
-- sentencia 2
-- sentencia 3
-- sentencia 4
-- sentencia 5
ROLLBACK
En este caso concreto, desharíamos las cinco sentencias ejecutadas desde el inicio.
Cuando creamos un checkpoint, podemos luego hacer un rollback parcial, regresando a un punto intermedio anterior en el tiempo, de tal manera que se descarta todo lo que venga después, pero dando la posibilidad de recuperar las sentencias SQL que se han aceptado hasta llegar a ese checkpoint.
En el caso de JDBC, tenemos las siguientes soluciones:
// Crea un punto de guardado normal y lo asigna a la variable savepoint.
Savepoint savepoint = conn.setSavepoint();
// Crea un punto de guardado con un nombre específico. Luego se puede referir
// a ese nombre desde SQL como parte de la sentencia ROLLBACK.
Savepoint savepoint2 = conn.setSavepoint("NOMBRE");
Finalmente, cuando llames a rollback() puedes pasarle como parámetro el Savepoint al que quieres volver:
conn.rollback(savepoint);