Utilizar Transaciones

Hay veces que no queremos que una sentencia tenga efecto a menos que otra también suceda. Por ejemplo, cuando el propietario del "The Coffee Break" actualiza la cantidad de café vendida semanalmente, también querrá actualizar la cantidad total vendida hasta la fecha. Sin embargo, el no querrá actualizar una sin actualizar la otra; de otro modo, los datos serían inconsistentes. La forma para asegurarnos que ocurren las dos acciones o que no ocurre ninguna es utilizar una transación. Una transación es un conjunto de una o más sentencias que se ejecutan como una unidad, por eso o se ejecutan todas o no se ejecuta ninguna.

Desactivar el modo Auto-entrega

Cuando se crea una conexión, está en modo auto-entrega. Esto significa que cada sentencia SQL individual es tratada como una transación y será automáticamente entregada justo después de ser ejecutada. (Para ser más preciso, por defecto, una sentencia SQL será entregada cuando está completa, no cuando se ejecuta. Una sentencia está completa cuando todas sus hojas de resultados y cuentas de actualización han sido recuperadas. Sin embargo, en la mayoría de los casos, una sentencia está completa, y por lo tanto, entregada, justo después de ser ejecutada).

La forma de permitir que dos o más sentencia sean agrupadas en una transación es desactivar el modo auto-entrega. Esto se demuestra en el siguiente código, donde con es una conexión activa:

con.setAutoCommit(false);

Entregar una Transación

Una vez que se ha desactivado la auto-entrega, no se entregará ninguna sentencia SQL hasta que llamemos explícitamente al método commit. Todas las sentencias ejecutadas después de la anterior llamada al método commit serán incluidas en la transación actual y serán entregadas juntas como una unidad. El siguiente código, en el que con es una conexión activa, ilustra una transación:
con.setAutoCommit(false);
PreparedStatement updateSales = con.prepareStatement(
			"UPDATE COFFEES SET SALES = ? WHERE COF_NAME LIKE ?");
updateSales.setInt(1, 50);
updateSales.setString(2, "Colombian");
updateSales.executeUpdate();
PreparedStatement updateTotal = con.prepareStatement(
	"UPDATE COFFEES SET TOTAL = TOTAL + ? WHERE COF_NAME LIKE ?");
updateTotal.setInt(1, 50);
updateTotal.setString(2, "Colombian");
updateTotal.executeUpdate();
con.commit();
con.setAutoCommit(true);
En este ejemplo, el modo auto-entrega se desactiva para la conexión con, lo que significa que las dos sentencias prepared updateSales y updateTotal serán entregadas juntas cuando se llame al método commit. Siempre que se llame al método commit (bien automáticamente, cuando está activado el modo auto-commit o explícitamente cuando está desactivado), todos los cambios resultantes de las sentencias de la transación serán permanentes. En este caso, significa que las columnas SALES y TOTAL para el café Colombian han sido cambiadas a 50 (si TOTAL ha sido 0 anteriormente) y mantendrá este valor hasta que se cambie con otra sentencia de actualización.

La línea final del ejemplo anterior activa el modo auto-commit, lo que significa que cada sentencia será de nuevo entregada automáticamente cuando esté completa. Volvemos por lo tanto al estado por defecto, en el que no tenemos que llamar al método commit. Es bueno desactivar el modo auto-commit sólo mientras queramos estar en modo transación. De esta forma, evitamos bloquear la base de datos durante varias sentencias, lo que incrementa los conflictos con otros usuarios.

Utilizar Transaciones para Preservar al Integridad de los Datos

Además de agrupar las sentencias para ejecutarlas como una unidad, las transaciones pueden ayudarnos a preservar la integridad de los datos de una tabla. Por ejemplo, supongamos que un empleado se ha propuesto introducir los nuevos precios de los cafés en la tabla COFFEES pero lo retrasa unos días. Mientras tanto, los precios han subido, y hoy el propietario está introduciendo los nuevos precios. Finalmente el empleado empieza a intrudir los precios ahora desfasados al mismo tiempo que el propietario intenta actualizar la tabla. Después de insertar los precios desfasados, el empleado se da cuenta de que ya no son válidos y llama el método rollback de la Connection para deshacer sus efectos. (El método rollback aborta la transación y restaura los valores que había antes de intentar la actualziación. Al mismo tiempo, el propietario está ejecutando una sentencia SELECT e imprime los nuevos precios. En esta situación, es posible que el propietario imprima los precios que más tarde serían devueltos a sus valores anteriores, haciendo que los precio impresos sean incorrectos.

Esta clase de situaciones puede evitarse utilizando Transaciones. Si un controlador de base de datos soporta transaciones, y casi todos lo hacen, proporcionará algún nivel de protección contra conflictos que pueden surgir cuando dos usuarios acceden a los datos a la misma vez.

Para evitar conflictos durante una transación, un controlador de base de datos utiliza bloqueos, mecanismos para bloquear el acceso de otros a los datos que están siendo accedidos por una transación. (Observa que en el modo auto-commit, donde cada sentencia es una transación, el bloqueo sólo se mantiene durante una sentencia). Una vez activado, el bloqueo permanece hasta que la transación sea entregada o anulada. Por ejemplo, un controlador de base de datos podría bloquear una fila de una tabla hasta que la actualización se haya entregado. El efecto de este bloqueo es evitar que usuario obtenga una lectura sucia, esto es, que lea un valor antes de que sea permanente. (Acceder a un valor actualizado que no haya sido entregado se considera una lectura sucia porque es posible que el valor sea devuelto a su valor anterior. Si leemos un valor que luego es devuelto a su valor antiguo, habremos leído un valor nulo).

La forma en que se configuran los bloqueos está determinado por lo que se llama nivel de aislamiento de transación, que pude variar desde no soportar transaciones en absoluto a soportar todas las transaciones que fuerzan una reglas de acceso muy estrictas.

Un ejemplo de nivel de aislamiento de transación es TRANSACTION_READ_COMMITTED, que no permite que se acceda a un valor hasta que haya sido entregado. En otras palabras, si nivel de aislamiento de transación se selecciona a TRANSACTION_READ_COMMITTED, el controlador de la base de datos no permitirá que ocurran lecturas sucias. El interface Connection incluye cinco valores que representan los niveles de aislamiento de transación que se pueden utilizar en JDBC.

Normalmente, no se necesita cambiar el nivel de aislamiento de transación; podemos utilizar el valor por defecto de nuestro controlador. JDBC permite averiguar el nivel de aislamiento de transación de nuestro controlador de la base de datos (utilizando el método getTransactionIsolation de Connection) y permite configurarlo a otro nivel (utilizando el método setTransactionIsolation de Connection). Sin embargo, ten en cuenta, que aunque JDBC permite seleccionar un nivel de aislamiento, hacer esto no tendrá ningún efecto a no ser que el driver del controlador de la base de datos lo soporte.

Cuándo llamar al método rollback

Como se mencionó anteriormente, llamar al método rollback aborta la transación y devuelve cualquier valor que fuera modificado a sus valores anteriores. Si estamos intentando ejecutar una o más sentencias en una transación y obtenemos una SQLException, deberíamos llamar al método rollback para abortar la transación y empezarla de nuevo. Esta es la única forma para asegurarnos de cuál ha sido entregada y cuál no ha sido entregada. Capturar una SQLException nos dice que hay algo erróneo, pero no nos dice si fue o no fue entregada. Como no podemos contar con el hecho de que nada fue entregado, llamar al método rollback es la única forma de asegurarnos.

Ozito