Las transacciones son un componente fundamental en las aplciaciones web modernas, ya que garantizan la atomicidad, consistencia, aislamiento y durabilidad (propiedades ACID) de las operaciones de los usuarios. Es crucial entender sus dos implementaciones principales: las transacciones locales y las distribuidas.
Transacciones Locales
Una transacción local se gestiona dentro de un único recurso, como una base de datos, y opera sobre la conexión JDBC específica a ese recurso. Su ámbito está limitado a dicho recurso, lo que la hace sencilla y eficiente para operaciones con una sola fuente de datos, aunque no puede coordinar cambios en múltiples bases de datos.
El siguiente ejemplo muestra una transferencia entre cuentas utilizando una transacción local:
public void realizarTransferenciaLocal() {
try (Connection conexion = obtenerOrigenDatos().getConnection()) {
// Desactivar el autocommit para iniciar una transacción explícita
conexion.setAutoCommit(false);
try (Statement sentencia = conexion.createStatement()) {
// Primer movimiento: débito en cuenta origen
sentencia.executeUpdate(
"UPDATE cuentas SET saldo = saldo - 300 WHERE id = 'ORIGEN'"
);
// Segundo movimiento: crédito en cuenta destino
sentencia.executeUpdate(
"UPDATE cuentas SET saldo = saldo + 300 WHERE id = 'DESTINO'"
);
// Si todo es correcto, confirmar los cambios en la base de datos
conexion.commit();
System.out.println("Transferencia local completada con éxito.");
} catch (SQLException e) {
// Deshacer todos los cambios en caso de error
conexion.rollback();
throw new RuntimeException("Error en la transferencia local", e);
}
} catch (SQLException e) {
throw new RuntimeException("No se pudo obtener la conexión", e);
}
}
En este código, el flujo consiste en establecer setAutoCommit(false) para iniciar un bloque transaccional, ejecutar las operaciones DML y finalmente invocar commit() si todo fue exitoso o rollback() si se produjo una excepción.
Transacciones Distribuidas
Las transacciones distribuidas están diseñadas para coordinar operaciones a través de múltiples recursos (bases de datos, sistemas de mensajería, etc.) garantizando su atomicidad. Esto se logra mediante un gestor de transacciones y el protocolo XA, que define la interacción entre el gestor y los diferentes gestores de recursos.
El siguiente código ilustra un escenaroi donde se modifica información en dos bases de datos distintas dentro de una sola transacción:
import javax.transaction.UserTransaction;
import javax.naming.InitialContext;
public void transferirEntreBaseDeDatos() {
UserTransaction transaccion = null;
try {
// Obtener el objeto de transacción desde el entorno JNDI
InitialContext contexto = new InitialContext();
transaccion = (UserTransaction) contexto.lookup("java:comp/UserTransaction");
// Obtener conexiones a las distintas bases de datos
try (Connection connOracle = obtenerDataSourceOracle().getConnection();
Connection connPostgres = obtenerDataSourcePostgres().getConnection()) {
transaccion.begin(); // Iniciar la transacción distribuida
// Operación en Oracle
Statement stmtOracle = connOracle.createStatement();
stmtOracle.executeUpdate(
"UPDATE inventario SET stock = stock - 10 WHERE producto = 'ABC'"
);
// Operación en PostgreSQL
Statement stmtPostgres = connPostgres.createStatement();
stmtPostgres.executeUpdate(
"INSERT INTO registro_ventas(producto, cantidad) VALUES ('ABC', 10)"
);
transaccion.commit(); // Confirmar ambos cambios de forma atómica
System.out.println("Transacción distribuida finalizada.");
}
} catch (Exception e) {
try {
if (transaccion != null) {
transaccion.rollback(); // Deshacer todos los cambios si hay un fallo
}
} catch (Exception rollbackEx) {
// Manejo de error durante el rollback
}
throw new RuntimeException("Fallo en la transacción distribuida", e);
}
}
La ejecución exitosa de commit() asegura que los cambios en ambos sistemas de bases de datos se apliquan de manera conjunta. Cualquier fallo durante el proceso desencadena un rollback() que revierte todas las operaciones participantes.