Hacer Actualizaciones por Lotes

Una actualización por lotes es un conjunto de varias sentencias de actualización que son enviadas a la base de datos para ser procesadas como un lote. Enviar múltiples sentencias de actualizaicón juntas a la base de datos puede, en algunas situaciones, ser mucho más eficiente que enviar cada sentencia separadamente. Esta posibilidad de enviar actualizaciones como una unidad, referida como facilidad de actualización por lotes, es una de las características proporcionadas por el API JDBC 2.0.

Utilizar Objetos Statement para Actualizaciones por Lotes

En el API JDBC 1.0, los objetos Statement enviaban actualizaciones a la base de datos individualmente con el método executeUpdate. Se pueden enviar varias sentencias executeUpdate en la misma transación, pero aunque son enviadas como una unidad, son procesadas individualmente. Los interfaces derivados de Statement, PreparedStatement y CallableStatement, tienen las mismas capacidades, utilizando sus propias versiones de executeUpdate.

Con el API JDBC 2.0, los objetos Statement, PreparedStatement y CallableStatement tienen la habilidad de mantener una lista de comandos que pueden ser enviados juntos como un lote. Ellos son creados con una lista asociada, que inicialmente está vacía. Se pueden añadir comandos SQL a esta lista con el método addBatch, y podemos vaciar la lista con el método clearBatch. Todos los comandos de la lista se envían a la base de datos con el método executeBatch. Ahora veamos como funcionan estos métodos.

Supongamos que nuestro propietario del café quiere traer nuevos cafés. Ha determinado que su mejor fuente es uno de sus actuales suministradores, Superior Coffee, y quiere añadir cuatro nuevos cafés a la tabla COFFEES. Cómo sólo va a insertar cuatro nuevas filas, la actualización por lotes podría no aumentar el rendimiento significativamente, pero es una buena oportunidad para demostrar la actualización por lotes. Recordamos que la tabla COFFEES tiene cinco columnas: COF_NAME del tipo VARCHAR(32), SUP_ID del tipo INTEGER, PRICE del tipo FLOAT, SALES del tipo INTEGER, y TOTAL del tipo INTEGER. Cada fila insertada tendrá valores para las cinco columnas en orden. El código para insertar una nueva fila como un lote se podría parecer a esto:

con.setAutoCommit(false);
Statement stmt = con.createStatement();
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Amaretto', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Hazelnut', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Amaretto_decaf', 49, 10.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Hazelnut_decaf', 49, 10.99, 0, 0)");
int [] updateCounts = stmt.executeBatch();
Ahora exáminemos el código línea por línea.
con.setAutoCommit(false);
Esta línea desactiva el modo auto-commit para el objeto Connection, con para que la transación no sea enviada o anulada automáticamente cuando se llame al método executeBatch. (Si no recuerdas qué era una transación, deberías revisar la página Transaciones). Para permitir un manejo de errores correcto, también deberíamos actualizar el modo auto-commit antes de empezar una actualización por lotes.
Statement stmt = con.createStatement();
Esta línea de código crea el objeto Statement, stmt. Al igual que todos los nuevos objetos Statement recien creados, stmt tiene una lista de comandos asociados y ésta lista está vacía.
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Amaretto', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Hazelnut', 49, 9.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Amaretto_decaf', 49, 10.99, 0, 0)");
stmt.addBatch("INSERT INTO COFFEES" +
	      "VALUES('Hazelnut_decaf', 49, 10.99, 0, 0)");
Cada una de estas líneas de código añade un comando a la lista de comandos asociados con stmt. Estos comandos son todos sentencias INSERT INTO, cada una añade una nueva fila que consiste en cinco valores de columna. Los valores para las columnas COF_NAME y PRICE se explican a sí mismos. el segundo valor de cada fila es 49 porque es el número de identificación del suministrador, Superior Coffee. Los últimos dos valores, las entradas para las columnas SALES y TOTAL, todas empiezan siendo cero porque todavía no se ha vendido nada. (SALES es el número de libras del café de esa columna vendidas la semana actual; y TOTAL es el número total de libras vendidas de este café).
int [] updateCounts = stmt.executeBatch();
En esta línea, stmt envía a la base de datos los cuatro comandos SQL que fueron añadidos a su lista de comandos para que sean ejecutados como un lote. Observa que stmt utiliza el método executeBatch para el lote de inserciones, no el método executeUpdate, que envía sólo un comando y devuelve una sóla cuenta de actualización. El controlador de la base de datos ejecuta los comandos en el orden en que fueron añadidos a la lista de comandos, por eso primero añadirá la fila de valores apra Amaretto, luego añade la fila de Hazelnut, luego Amaretto decaf, y finalmente Hazelnut decaf. Si los cuatro comandos se ejecutan satisfactoriamente, el controlador de la base de datos devolverá una cuenta de actualización para cada comando en el orden en que fue ejecutado. Las cuentas de actualización, indicarán cuántas líneas se vieron afectadas por cada comando, y se almacenan en el array de enteros updateCounts.

En este punto, updateCounts debería contener cuatro elementos del tipo int. En este caso, cada uno de ellos será 1 porque una inserción afecta a un fila. La lista de comandos asociados con stmt ahora estará vacía porque los cuatro comandos añadidos anteriormente fueron enviados a la base de datos cuando stmt llamó al método executeBatch. En cualquier momento podemos vaciar la lista de comandos con el método clearBatch.

Excepciones en las Actualizaciones por Lotes

Existen dos excepciones que pueden ser lanzadas durante una actualización por lotes: SQLException y BatchUpdateException.

Todos los métodos del API JDBC lanzarán un objeto SQLException si existe algún problema de acceso a la base de datos. Además, el método executeBatch lanzará una SQLException si hemos utilizado el método addBatch para añadir un comando y devuelve una hoja de resultados al lote de comandos que está siendo ejecutado. Típicamente una petición (una sentencia SELECT) devolverá una hoja de resultados, pero algunos métodos, como algunos de DatabaseMetaData pueden devolver una hoja de datos.

Sólo utilizar el método addBatch para añadir un comando que produce una hoja de resultados no hace que se lance una excepción. No hay problema mientras el comando está siendo situado en la lista de comandos de un objeto Statenment. Pero habrá problemas cuando el método executeBatch envíe el lote al controlador de la base de datos para ejecutarlo. Cuando se ejecuta cada comando, debe devolver una cuenta de actualización que pueda ser añadida al array de cuentas de actualización devuelto por el método executeBatch. Intentar poner una hoja de resultados en un array de cuentas de actualización causará un error y hará que executeBatch lance una SQLException. En otras palabras, sólo los comandos que devuelven cuentas de actualización (comandos como INSERT INTO, UPDATE, DELETE, CREATE TABLE, DROP TABLE, ALTER TABLE, etc) pueden ser ejecutados como un lote con el método executeBatch.

Si no se lanzó una SQLException, sabremos que no hubo problemas de acceso y que todos los comandos produjeron cuentas de actualización. Si uno de los comandos no puede ser ejecutado por alguna razón, el método executeBatch lanzará una BatchUpdateException. Además de la información que tienen todas las excepciones, este excepción contiene un array de cuentas de actualización para los comandos que se ejecutaron satisfactoriamente antes de que se lanzara la excepción. Cómo las cuentas de actualización están en el mismo orden que los comandos que las produjeron, podremos decir cuántos comandos fueron ejecutados y cuáles fueron.

BatchUpdateException desciende de SQLException. Esto significa que utiliza todos los métodos disponibles en un objeto SQLException. El siguiente fragmento de código imprime la información de SQLException y las cuentas de actualización contenidas en un objeto BatchUpdateException. Como getUpdateCounts devuelve un array de int, utiliza un bucle for para imprimir todas las cuentas de actualización.

try {
	// make some updates
} catch(BatchUpdateException b) {
	System.err.println("SQLException: " + b.getMessage());
	System.err.println("SQLState:  " + b.getSQLState());
	System.err.println("Message:  " + b.getMessage());
	System.err.println("Vendor:  " + b.getErrorCode());
	System.err.print("Update counts:  ");
	int [] updateCounts = b.getUpdateCounts();
	for (int i = 0; i < updateCounts.length; i++) {
		System.err.print(updateCounts[i] + "   ");
	}
}
Para ver un programa completo de actualización por lotes, puedes ver BatchUpdate.java. El código pone juntos los fragmentos de código de las páginas anteriores y crea un programa completo. Podrías observar que hay dos bloques catch al final del código. Si hay un objeto BatchUpdateException el primer bloque lo capturará. El segundo bloque capturará un objeto SQLException que no sea un objeto BatchUpdateException.

Ozito