2.1 Tutorial de JDBC Índice n n n n n n n Introducción Accesos básicos Tipos SQL y Java DataSources Pool de conexiones Transacciones Otros temas Introducción n n JDBC (Java DataBase Connectivity) es un API que permite lanzar queries a una base de datos relacional Su diseño está inspirado en dos conocidas APIs n n n ODBC (Open DataBase Connectivity) X/OPEN SQL CLI (Call Level Interface) El programador siempre trabaja contra los paquetes java.sql y javax.sql n Forma parte de J2SE n n n javax.sql formaba parte de J2EE, pero se movió a J2SE (desde la versión 1.4 de J2SE) Contienen un buen número de interfaces y algunas clases concretas, que conforman el API de JDBC Para poder conectarse a la BD y lanzar queries, es preciso tener un driver adecuado para ella n n Un driver suele ser un fichero .jar que contiene una implementación de todos los interfaces del API de JDBC Nuestro código nunca depende del driver, dado que siempre trabaja contra los paquetes java.sql y javax.sql Driver JDBC Aplicación <<use>> <<use>> java.sql BD <<access>> javax.sql Driver JDBC Tipos de drivers (1) Driver Tipo 1 (ej.: bridge JDBC-ODBC) <<use>> API nativa estándar (ej.: ODBC) <<access>> Driver Tipo 2 (ej: Oracle OCI) <<use>> API nativa BD (normalmente en C/C++) <<access>> <<access>> Driver Tipo 3 Driver Tipo 4 (ej: Oracle thin) <<use>> Servidor con API genérica <<access>> BD Tipos de drivers (y 2) n Comentarios n n n Los drivers de tipo 1 y 2 llaman a APIs nativas mediante JNI (Java Native Interface) => requieren que la máquina en la que corre la aplicación tenga instaladas las librerías de las APIs nativas (.DLL, .so) Los drivers de tipo 3 y 4 son drivers 100% Java La ventaja de un driver de tipo 3 es que una aplicación puede usarlo para conectarse a varias BDs n n Pero esto también se puede conseguir usando un driver distinto (de los otros tipos) para cada BD Eficiencia: en general, los drivers más eficientes son los de tipo 2 Independencia de la BD n n Idealmente, si nuestra aplicación cambia de BD, no necesitamos cambiar el código; simplemente, necesitamos otro driver Sin embargo, desafortunadamente las BDs relacionales usan distintos dialectos de SQL (¡ a pesar de que en teoría es un estándar !) n n n Tipos de datos: varían mucho según la BD Generación de identificadores: secuencias, autonumerados, etc. Veremos patrones para hacer frente a este problema n Usaremos interfaces para el acceso a BD, de manera que se puedan construir adaptadores para distintas BDs, proporcionando implementaciones por defecto con SQL estándar cuando sea posible Ejemplo de actualización: es.udc.fbellas.j2ee.jdbctutorial.InsertExample (1) package es.udc.fbellas.j2ee.jdbctutorial; import import Import import java.sql.Connection; java.sql.Statement; java.sql.DriverManager; java.sql.SQLException; public final class InsertExample { public static void main (String[] args) { Connection connection = null; Statement statement = null; try { /* Get String String String String a connection. */ driverClassName = "com.mysql.jdbc.Driver"; driverUrl = "jdbc:mysql://localhost/j2ee"; user = "j2ee"; password = "j2ee"; Class.forName(driverClassName); connection = DriverManager.getConnection(driverUrl, user, password); Ejemplo de actualización: es.udc.fbellas.j2ee.jdbctutorial.InsertExample (2) /* Create data for some accounts. */ String[] accountIdentifiers = new String[] {"fbellas-1", "fbellas-2", "fbellas-3"}; double[] balances = new double[] {100.0, 200.0, 300.0}; /* Create "statement". */ statement = connection.createStatement(); /* Insert the accounts in database. */ for (int i=0; i<accountIdentifiers.length; i++) { /* Execute query. */ String queryString = "INSERT INTO TutAccount " + "(accId, balance) VALUES ('" + accountIdentifiers[i] + "', " + balances[i] + ")"; int insertedRows = statement.executeUpdate(queryString); if (insertedRows != 1) { throw new SQLException(accountIdentifiers[i] + ": problems when inserting !!!!"); } } Ejemplo de actualización: es.udc.fbellas.j2ee.jdbctutorial.InsertExample (y 3) } catch (SQLException e) { e.printStackTrace(); } finally { try { if (statement != null) { statement.close(); } if (connection != null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } // try } // main } // class Ejemplo de búsqueda: es.udc.fbellas.j2ee.jdbctutorial.SelectExample (1) public final class SelectExample { public static void main (String[] args) { Connection connection = null; Statement statement = null; ResultSet resultSet = null; try { /* Get a connection. */ << ... >> /* Create "statement". */ statement = connection.createStatement(); /* Execute query. */ String queryString = "SELECT accId, balance FROM " + "TutAccount"; resultSet = statement.executeQuery(queryString); Ejemplo de búsqueda: es.udc.fbellas.j2ee.jdbctutorial.SelectExample (2) /* Iterate over matched rows. */ while (resultSet.next()) { int i = 1; String accountIdentifier = resultSet.getString(i++); double balance = resultSet.getDouble(i++); System.out.println("accountIdentifier = " + accountIdentifier + " | balance = " + balance); } } catch (SQLException e) { e.printStackTrace(); Ejemplo de búsqueda: es.udc.fbellas.j2ee.jdbctutorial.SelectExample (y 3) } finally { try { if (resultSet != null) { resultSet.close(); } if (statement != null) { statement.close(); } if (connection != null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } // try } // main } // class Comentarios (1) n Cargar el driver n n En una aplicación real n n n Su inicializador static registra el driver en DriverManager El nombre de la clase del driver, la URL (depende del driver), el nombre de usuario y la contraseña deberían ser configurables (ej.: leerlos de un fichero de configuración) Existe una alternativa mejor a DriverManager (javax.sql.DataSource) ResultSet es un proxy sobre las filas que han concordado en la búsqueda n El driver no tiene porque traer “de golpe” todas las filas a memoria n Un buen driver leerá las filas por bloques, leyendo un nuevo bloque cada vez que se intenta leer una fila que no está en memoria (quizás eliminando de memoria el bloque anterior) Comentarios (2) DriverManager 0..n <<Interface>> Driver <<instantiate>> <<Interface>> Statement +manages <<instantiate>> <<Interface>> Connection <<instantiate>> <<Interface>> ResultSet Comentarios (y 3) n Liberación de recursos n n En principio, aunque no se llame a Connection.close, cuando la conexión sea eliminada por el garbage collector, el método finalize de la clase que implementa Connection, invocará al método close Además n n n Cuando se cierra una conexión, cierra todos sus Statements asociados Cuando se cierra un Statement, cierra todos sus ResultSets asociados Sin embargo, n n En una aplicación multi-thread que solicita muchas conexiones por minuto (ej.: una aplicación Internet), el garbage collector correrá demasiado tarde => es imprescindible cerrar las conexiones tan pronto como se pueda Puede haber bugs en algunos drivers, de manera que no cierren los Statements asociados a una conexión y los ResultSets asociados a un Statement => mejor cerrarlos explícitamente PreparedStatement n Cada vez que se envía una query a la BD, ésta construye un plan para ejecutarla n n n n Ejemplo InsertExample n n n n La parsea Determina qué se quiere hacer Determina cómo ejecutarla Existe un bucle en el que repetidamente se lanza la misma query INSERT con distintos parámetros ¡ La BD construye un plan para ejecutar la misma query cada vez ! En este tipo de situaciones, es mejor usar PreparedStatement PreparedStatement n n Es un interfaz que deriva de Statement Permite representar una query parametrizada, para la que la BD construirá un plan es.udc.fbellas.j2ee.jdbctutorial.PreparedStatementExample (1) public final class PreparedStatementExample { public static void main (String[] args) { Connection connection = null; PreparedStatement preparedStatement = null; try { /* Get a connection. */ << ... >> /* Create data for some accounts. */ String[] accountIdentifiers = new String[] {"fbellas-1", "fbellas-2", "fbellas-3"}; double[] balances = new double[] {100.0, 200.0, 300.0}; es.udc.fbellas.j2ee.jdbctutorial.PreparedStatementExample (2) /* Create "preparedStatement". */ String queryString = "INSERT INTO TutAccount " + "(accId, balance) VALUES (?, ?)"; preparedStatement = connection.prepareStatement(queryString); /* Insert the accounts in database. */ for (int i=0; i<accountIdentifiers.length; i++) { /* Fill "preparedStatement". */ int j = 1; preparedStatement.setString(j++, accountIdentifiers[i]); preparedStatement.setDouble(j++, balances[i]); /* Execute query. */ int insertedRows = preparedStatement.executeUpdate(); if (insertedRows != 1) { throw new SQLException(accountIdentifiers[i] + ": problems when inserting !!!!"); } } es.udc.fbellas.j2ee.jdbctutorial.PreparedStatementExample (y 3) } catch (SQLException e) { e.printStackTrace(); } finally { try { if (preparedStatement != null) { preparedStatement.close(); } if (connection != null) { connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } // try } // main } // class Statement vs PreparedStatement n n PreparedStatement es más eficiente en bucles que lanzan la misma query con distintos parámetros Statement obliga a formatear los datos de la query porque ésta se envía a la BD tal cual la escribimos n n n n Las cadenas de caracteres tienen que ir entre ‘’ => código menos legible y más propenso a errores Existen algunos tipos de datos (ej.: fechas) que se especifican de manera distinta según la BD Con PreparedStatement el formateo de los datos lo hace el driver Conclusión n En general, usar siempre PreparedStatement Tipos SQL y Java n ResultSet y PreparedStatement proporcionan métodos getXXX y setXXX n n n ¿ Cuál es la correspondencia entre tipos Java y tipos SQL ? Idea básica: un dato de tipo Java se puede almacenar en una columna cuyo tipo SQL sea consistente con el tipo Java Las BDs suelen emplear nombres distintos para los tipos que proporcionan n No afecta al código Java (excepto que cree tablas, lo que en general, no debe hacerse) Correspondencia entre tipos Java y SQL estándar Tipo Java boolean Tipo SQL BIT byte TINYINT short SMALLINT int INTEGER long BIGINT float REAL double DOUBLE java.math.BigDecimal NUMERIC String VARCHAR o LONGVARCHAR byte[] VARBINARY o LONGVARBINARY java.sql.Date DATE java.sql.Time TIME java.sql.Timestamp TIMESTAMP DataSources (1) n Cargar el driver y obtener conexiones con DriverManager.getConnection requiere n n n n Conocer el nombre de la clase del driver La cadena de conexión A no ser que leamos los anteriores parámetros de un fichero de configuración, el registro del driver y la obtención de conexiones no será portable Interfaz javax.sql.DataSource n n n Es la alternativa recomendada a DriverManager.getConnection No hay que escribir código para registrar el driver Actúa como factoría de conexiones DataSources (2) n n Normalmente una aplicación obtiene una referencia a un objeto DataSource mediante JNDI JNDI (Java Naming and Directory Interface) n n n n Familia de paquetes javax.naming Formó parte de J2EE (hasta la versión 1.2), pero se movió a J2SE (desde la versión 1.3 de J2SE) Es un API que proporciona un conjunto de interfaces para acceder a un servicio de nombres y directorios Servicio de nombres y directorios n Permite gestionar información de manera jerárquica n n Usuarios (login+password, directorios home, etc.), máquinas de la red (direcciones ethernet e IP, etc.) Puede implementarse como un sólo servidor o una jerarquía de servidores (federación) DataSources (3) n Servicio de nombres y directorios n Ejemplos n n n n JNDI permite acceder a cualquier servicio de nombres y directorios usando la misma interfaz n n LDAP (Lightweight Directory Access Protocol) NIS/NIS+ (Network Information System) Microsoft Active Directory Se necesita disponer de una implementación de JNDI para el servicio en cuestión Actualmente se pretende que las aplicaciones lean la información de configuración mediante servicios de nombres y directorios, y no mediante ficheros planos locales n Ventaja: la información de configuración de las aplicaciones se puede centralizar en la máquina (o máquinas) que corren el servicio de nombres y directorios DataSources (y 4) n Ejemplo import javax.naming.Context import javax.naming.InitialContext; import javax.sql.DataSource; // ... Context context = new InitialContext(); DataSource dataSource = (DataSource)context.lookup(“java:comp/env/jdbc/J2EE-ExamplesDS”); Connection connection = dataSource.getConnection(); n ¿ Quién registra los objetos DataSource en el servicio de nombres y directorios ? n n Podría hacerlo el programador usando JDNI Sin embargo, normalmente dispondremos de una herramienta de administración o un fichero de configuración en el servidor Una implementación sencilla de un DataSource n n A partir del tema 4 (tecnologías web) usaremos JNDI para localizar objetos DataSource Mientras tanto ... <<Interface>> DataSource (from sql) SimpleDataSource <<use>> + SimpleDataSource() ConfigurationParametersManager (from configuration) Sólo implementa "getConnection()". El resto de operaciones lanzan la exception (unchecked) "UnsupportedOperationException" es.udc.fbellas.j2ee.util.sql.SimpleDataSource (1) public class SimpleDataSource implements DataSource { private static final String DRIVER_CLASS_NAME_PARAMETER = "SimpleDataSource/driverClassName"; private static final String URL_PARAMETER = "SimpleDataSource/url"; private static final String USER_PARAMETER = "SimpleDataSource/user"; private static final String PASSWORD_PARAMETER = "SimpleDataSource/password"; private static String url; private static String user; private static String password; es.udc.fbellas.j2ee.util.sql.SimpleDataSource (2) static { try { /* Read configuration parameters. */ String driverClassName = ConfigurationParametersManager.getParameter( DRIVER_CLASS_NAME_PARAMETER); url = ConfigurationParametersManager.getParameter( URL_PARAMETER); user = ConfigurationParametersManager.getParameter( USER_PARAMETER); password = ConfigurationParametersManager.getParameter( PASSWORD_PARAMETER); /* Load driver. */ Class.forName(driverClassName); } catch (Exception e) { e.printStackTrace(); } } // static es.udc.fbellas.j2ee.util.sql.SimpleDataSource (y 3) public Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } << Resto de métodos => throw new UnsupportedOperationException("Not implemented"); >> } // class es.udc.fbellas.j2ee.util.configuration.ConfigurationParametersManager (1) public final class ConfigurationParametersManager { private static final String JNDI_PREFIX = "java:comp/env/"; private static final String CONFIGURATION_FILE = "ConfigurationParameters.properties"; private static boolean usesJNDI; private static Map parameters; static { try { /* Read property file (if exists).*/ Class configurationParametersManagerClass = ConfigurationParametersManager.class; ClassLoader classLoader = configurationParametersManagerClass.getClassLoader(); InputStream inputStream = classLoader.getResourceAsStream(CONFIGURATION_FILE); Properties properties = new Properties(); properties.load(inputStream); inputStream.close(); es.udc.fbellas.j2ee.util.configuration.ConfigurationParametersManager (2) /* We have been able to read the file. */ usesJNDI = false; System.out.println("*** Using " + "'ConfigurationParameters.properties' file " + "for configuration ***"); /* * We use a "HashMap" instead of a "HashTable" because * HashMap's methods are *not* synchronized (so they are * faster), and the parameters are only read. */ parameters = new HashMap(properties); } catch (Exception e) { /* We assume configuration with JNDI. */ usesJNDI = true; System.out.println("*** Using JNDI for configuration ***"); /* * We use a synchronized map because it will be filled * by using a lazy strategy. */ parameters = Collections.synchronizedMap(new HashMap()); } } // static es.udc.fbellas.j2ee.util.configuration.ConfigurationParametersManager (y 3) private ConfigurationParametersManager() {} public static String getParameter(String name) throws MissingConfigurationParameterException { String value = (String) parameters.get(name); if (value == null) { if (usesJNDI) { try { InitialContext initialContext = new InitialContext(); value = (String) initialContext.lookup( JNDI_PREFIX + name); parameters.put(name, value); } catch (Exception e) { throw new MissingConfigurationParameterException( name); } } else { throw new MissingConfigurationParameterException(name); } } return value; } } // class Comentarios n n ConfigurationParameters.properties tiene que estar en el class path Ejemplo de especificación de parámetros de configuración en ConfigurationParameters.properties # -----------------------------------------------------------# SimpleDataSource. # -----------------------------------------------------------SimpleDataSource/driverClassName=com.mysql.jdbc.Driver SimpleDataSource/url=jdbc:mysql://localhost/j2ee SimpleDataSource/user=j2ee SimpleDataSource/password=j2ee Pool de conexiones (1) n En una aplicación servidora (ej.: un contenedor de páginas JSP/Servlets, un contenedor de EJBs) con carga alta n n n n Se solicitan muchas conexiones a la BD por minuto Pedir una conexión cada vez se convierte en un cuello de botella Solución: pool de conexiones Ejemplo de implementación <<interface>> java.sql.Connection ConnectionWrapper - c : java.sql.Connection <<instantiate>> <<interface>> javax.sql.DataSource ConnectionPool <<instantiate>> - connections: java.util.LinkedList releaseConnection(c:Connection):void Pool de conexiones (2) n Ejemplo de implementación (cont) n ConnectionPool n n n Una sóla instancia Cuando se crea, pide “n” conexiones a la BD (quizás usando DriverManager.getConnection) y las almacena en una lista getConnection n n n releaseConnection (con visibilidad “package”) n n Si quedan conexiones en la lista, devuelve una de ellas dentro de un objeto ConnectionWrapper (comprobando antes si está “viva”) En otro caso, deja durmiendo al thread llamador usando wait Devuelve la conexión a la lista, y notifica (notifyAll) a los posibles threads que esperan por una conexión ConnectionWrapper n n Encapsula a una conexión real close n n finalize n n Usa releaseConnection para devolver la conexión real al pool Si no se ha llamado a ConnectionWraper.close, lo llama Resto de operaciones n Delegan en la conexión real Pool de conexiones (y 3) n Ejemplo de implementación (cont) n n n El programador trabaja contra javax.sql.DataSource y java.sql.Connection Su código no sabe que está trabajando contra un pool de conexiones Los servidores de aplicaciones completos J2EE proporcionan implementaciones de DataSources con pool de conexiones n A partir del tema 4 (tecnologías web), trabajaremos con pool de conexiones Transacciones n n Permiten ejecutar bloques de código con las propiedades ACID (Atomicity-Consistency-IsolationDurability) Por defecto, cuando se crea una conexión está en modo auto-commit n n Cada Statement y PreparedStatement que se ejecute sobre ella irá en su propia transacción Para ejecutar varias queries en una misma transacción es preciso n n n Deshabilitar el modo auto-commit de la conexión Lanzar las queries Terminar con connection.commit() si todo va bien, o connection.rollback() en otro aso. es.udc.fbellas.j2ee.jdbctutorial.TransactionExample (1) n Mismo ejemplo que es.udc.fbellas.j2ee.jdbctutorial.PreparedStatementExample, pero ahora la inserción de cuentas se realiza en una única transacción public final class TransactionExample { public static void main (String[] args) { Connection connection = null; PreparedStatement preparedStatement = null; boolean commited = false; try { /* Get a connection with autocommit to "false". */ DataSource dataSource = new SimpleDataSource(); connection = dataSource.getConnection(); connection.setAutoCommit(false); << Insertar cuentas. >> /* Commit transaction. */ connection.commit(); commited = true; es.udc.fbellas.j2ee.jdbctutorial.TransactionExample (y 2) } catch (SQLException e) { e.printStackTrace(); } finally { try { if (preparedStatement != null) { preparedStatement.close(); } if (connection != null) { if (!commited) { connection.rollback(); } connection.close(); } } catch (SQLException e) { e.printStackTrace(); } } // try } // main } // class Problemas de concurrencia n En principio, las transacciones garantizan que no hay problemas si dos o más transacciones modifican datos comunes concurrentemente n n n Propiedad “I” en ACID Para el programador es como si la BD serializase las transacciones En realidad, dependiendo de la estrategia de bloqueo que se use, pueden darse los siguientes problemas n n n Dirty reads Non-repeatable reads Phantom reads Dirty reads n Una transacción lee datos actualizados por otra transacción, pero todavía no comprometidos Transacción 1 Transacción 2 begin SELECT X /* X = 0 */ X = X + 10 /* X = 10 */ UPDATE X /* X = 10 en BD pero no comprometido */ begin SELECT X /* X = 10 */ rollback /* X = 0 en BD */ X = X + 10 /* X = 20 */ UPDATE X /* X = 20 en BD pero no comprometido */ commit /* X = 20 en BD y comprometido */ Non-repeatable reads n Una transacción relee un dato que ha cambiado “mágicamente” desde la primera lectura Transacción 1 Transacción 2 begin SELECT X /* X = 0 */ begin X = 20 UPDATE X /* X = 20 en BD pero no comprometido */ commit /* X = 20 en BD y comprometido */ SELECT X /* X = 20 */ ... commit Phantom reads n Una transacción relanza una query de consulta y obtiene “mágicamente” más filas la segunda vez Transacción 1 Transacción 2 begin SELECT WHERE condition begin INSERT /* Inserta nuevas filas en BD (no comprometidas), algunas cumpliendo “condition” */ commit /* Inserciones comprometidas */ SELECT WHERE condition /* Ahora devuelve más filas */ ... commit Transaction isolation levels (1) n java.sql.Connection proporciona el método setTransactionIsolation, que permite especificar el nivel de aislamiento deseado n n n n n n TRANSACTION_NONE: transacciones no soportadas TRANSACTION_READ_UNCOMMITED: pueden ocurrir “dirty reads”, “non-repeatable reads” y “phantom reads” TRANSACTION_READ_COMMITED: pueden ocurrir “non-repeatable reads” y “phantom reads” TRANSACTION_REPEATABLE_READ: pueden ocurrir “phantom reads” TRANSACTION_SERIALIZABLE: elimina todos los problemas de concurrencia Mayor nivel de aislamiento => la BD realiza más bloqueos => menos concurrencia Transaction isolation levels (y 2) n No todos los drivers/BDs tienen porque soportar todos los niveles de aislamiento n n Suelen soportar TRANSACTION_READ_COMMITED y TRANSACTION_SERIALIZABLE ¿ Cuál es el nivel de aislamiento por defecto en las conexiones ? n n Depende del driver/BD Suele ser TRANSACTION_READ_COMMITED Una transferencia bancaria n Ejemplos es.udc.fbellas.j2ee.jdbctutorial.Transf erence*Example n n n Realizan una transferencia de dinero entre dos cuentas bancarias Veremos 4 estrategias Objetivo n Estudiar los problemas de concurrencia que puede tener una transacción típica que n n n n Lee un dato (o más) de BD Lo actualizar en memoria (ej.: le suma una cantidad) Lo actualiza en BD Muchas transacciones son de este estilo es.udc.fbellas.j2ee.jdbctutorial.Transference1Example (1) private static void transfer(Connection connection, String sourceAccountIdentifier, String destinationAccountIdentifier, double amount) throws SQLException { boolean commited = false; try { connection.setAutoCommit(false); addAmount(connection, sourceAccountIdentifier, -amount); addAmount(connection, destinationAccountIdentifier, amount); connection.commit(); commited = true; connection.setAutoCommit(true); } finally { if (!commited) { connection.rollback(); } } } es.udc.fbellas.j2ee.jdbctutorial.Transference1Example (2) private static void addAmount(Connection connection, String accountIdentifier, double amount) throws SQLException { PreparedStatement preparedStatement = null; try { /* Create "preparedStatement". */ String queryString = "UPDATE TutAccount" + " SET balance = balance + ? WHERE accId = ?"; preparedStatement = connection.prepareStatement(queryString); /* Fill "preparedStatement". */ int i = 1; preparedStatement.setDouble(i++, amount); preparedStatement.setString(i++, accountIdentifier); es.udc.fbellas.j2ee.jdbctutorial.Transference1Example (y 3) /* Execute query. */ int updatedRows = preparedStatement.executeUpdate(); if (updatedRows == 0) { throw new SQLException(accountIdentifier + “ not found"); } } finally { if (preparedStatement != null) { preparedStatement.close(); } } } } n NOTA: En una situación real usaríamos InstanceNotFoundException o similar en vez de SQLException(accountIdentifier + “ not found”) Comentarios n n Asumiendo conexión con TRANSACTION_READ_COMMITED (que es lo normal) o superior => no hay problemas de concurrencia Sin embargo, el código anterior no puede detectar de una manera elegante y portable la situación de “cuenta origen con balance insuficiente” n Se puede añadir una restricción de integridad (balance >= 0) sobre la columna balance Si se viola => el primer UPDATE provocará una SQLException n Problemas: n n n ¿ cómo sabe el programador si ha habido un problema en la BD (ej.: se ha caído) o si la cuenta no tiene suficiente balance ? ¿ Y si quisiésemos actualizar otros datos de la cuenta ? es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (1) private static void transfer(Connection connection, String sourceAccountIdentifier, String destinationAccountIdentifier, double amount) throws SQLException, IOException { boolean commited = false; try { /* Prepare connection. */ connection.setAutoCommit(false); /* Read balances. */ double sourceAccountBalance = getBalance(connection, sourceAccountIdentifier); double destinationAccountBalance = getBalance(connection, destinationAccountIdentifier); /* Calculate new balances. */ if (sourceAccountBalance < amount) { throw new SQLException(sourceAccountIdentifier + ": Insufficient balance"); } es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (2) double sourceAccountNewBalance = sourceAccountBalance - amount; double destinationAccountNewBalance = destinationAccountBalance + amount; /* Update accounts. */ updateBalance(connection, sourceAccountIdentifier, sourceAccountNewBalance); updateBalance(connection, destinationAccountIdentifier, destinationAccountNewBalance); /* Commit. */ connection.commit(); commited = true; connection.setAutoCommit(true); } finally { if (!commited) { connection.rollback(); } } } es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (3) private static double getBalance(Connection connection, String accountIdentifier) throws SQLException { PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { /* Create "preparedStatement". */ String queryString = "SELECT balance FROM TutAccount WHERE“ + " accId = ?"; preparedStatement = connection.prepareStatement(queryString); /* Fill "preparedStatement". */ int i = 1; preparedStatement.setString(i++, accountIdentifier); /* Execute query. */ resultSet = preparedStatement.executeQuery(); es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (4) if (!resultSet.next()) { throw new SQLException(accountIdentifier + " not found"); } /* Get results. */ return resultSet.getDouble(1); } finally { if (resultSet != null) { resultSet.close(); } if (preparedStatement != null) { preparedStatement.close(); } } } es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (5) private static void updateBalance (Connection connection, String accountIdentifier, double newBalance) throws SQLException { PreparedStatement preparedStatement = null; try { /* Create "preparedStatement". */ String queryString = "UPDATE TutAccount" + " SET balance = ? WHERE accId = ?"; preparedStatement = connection.prepareStatement(queryString); /* Fill "preparedStatement". */ int i = 1; preparedStatement.setDouble(i++, newBalance); preparedStatement.setString(i++, accountIdentifier); /* Execute query. */ int updatedRows = preparedStatement.executeUpdate(); es.udc.fbellas.j2ee.jdbctutorial.Transference2Example (y 6) if (updatedRows == 0) { throw new SQLException(accountIdentifier + " not found"); } } finally { if (preparedStatement != null) { preparedStatement.close(); } } } n NOTA: En una situación real usaríamos InstanceNotFoundException e InsufficientBalanceException o similares en vez de los SQLException(...) Comentarios (1) n La solución es más genérica n n Problema n n Podríamos tener getAccountState y updateAccountState en vez de getBalance y updateBalance, que nos valdrían para implementar transfer y otras operaciones que lean y/o actualicen datos de las cuentas Si la conexión tiene un nivel inferior a TRANSACTION_SERIALIZABLE => problemas de concurrencia Ejemplo n n Dos transacciones ejecutando transfer(connection, “fbellas-1”, “fbellas-2”, 10) Supongamos inicialmente => (fbellas-1, 100) y (fbellas2, 200) Comentarios (2) Transacción 1 Transacción 2 Leer balances => sourceAccountBalance = 100 destinationAccountBalance = 200 Leer balances => sourceAccountBalance = 100 destinationAccountBalance = 200 Calcular nuevos balances => sourceAccountNewBalance = 90 destinationAccountNewBalance = 210 Calcular nuevos balances => sourceAccountNewBalance = 90 destinationAccountNewBalance = 210 Actualizar cuentas => fbellas-1 => balance = 90 fbellas-2 => balance = 210 Actualizar cuentas => fbellas-1 => balance = 90 fbellas-2 => balance = 210 n n Problema: hemos perdido los efectos de una de las transacciones El problema puede darse siempre que se hagan varias transferencias simultáneas que tengan al menos una cuenta en común Comentarios (y 3) n Experimentar ejecutando ... java –classpath $J2EE_EXAMPLES_CLASSPATH es.udc.fbellas.j2ee.jdbctutorial.Transference2Example fbellas-1 fbellas-2 ... desde dos ventanas n NOTA: Las cuentas fbellas-1 y fbellas-2 tienen que estar creadas y con balance >= 20 es.udc.fbellas.j2ee.jdbctutorial.Transference3Example n Idem Transference2Example, excepto private static double getBalance(Connection connection, String accountIdentifier, boolean forUpdate) throws SQLException { PreparedStatement preparedStatement = null; ResultSet resultSet = null; try { /* Create "preparedStatement". */ String queryString = "SELECT balance FROM TutAccount WHERE“ + " accId = ?"; if (forUpdate) { queryString += " FOR UPDATE"; } // ... Comentarios (1) n n .. y la implementación de transfer usa getBalance(accountIdentifier, true) No tiene problemas de concurrencia n n n SELECT FOR UPDATE bloquea la fila (filas) en cuestión hasta que termine la transacción En el ejemplo anterior, si la transacción 1 ejecuta antes el SELECT FOR UPDATE sobre fbellas-1, cuando la segunda transacción intente hacer lo mismo, se quedará bloqueada hasta que termina la primera Si hubiese dos transferencias concurrentes del estilo .. transfer(connection, “fbellas-1”, “fbellas-2”, 10) n transfer(connection, “fbellas-2”, “fbellas-1”, 10) ... podría producirse una situación de inter-bloqueo => una de las transacciones gana, y la otra recibe un SQLException (la BD queda en estado consistente) n Comentarios (y 2) n Experimentar ejecutando ... java –classpath $J2EE_EXAMPLES_CLASSPATH es.udc.fbellas.j2ee.jdbctutorial.Transference3Example fbellas-1 fbellas-2 n ... desde dos ventanas ... y ... java –classpath $J2EE_EXAMPLES_CLASSPATH es.udc.fbellas.j2ee.jdbctutorial.Transference3Example fbellas-1 fbellas-2 java –classpath $J2EE_EXAMPLES_CLASSPATH es.udc.fbellas.j2ee.jdbctutorial.Transference3Example fbellas-2 fbellas-1 ... cada uno en su ventana es.udc.fbellas.j2ee.jdbctutorial.Transference4Example (1) n Idem Transference2Example, excepto private static void transfer(Connection connection, String sourceAccountIdentifier, String destinationAccountIdentifier, double amount) throws SQLException, IOException { boolean commited = false; try { /* * Prepare connection. * * IMPORTANT: Some JDBC drivers require * "setTransactionIsolation to be called before * "setAutoCommit". */ int oldTransactionIsolation = connection.getTransactionIsolation(); connection.setTransactionIsolation( Connection.TRANSACTION_SERIALIZABLE); connection.setAutoCommit(false); es.udc.fbellas.j2ee.jdbctutorial.Transference4Example (y 2) << Igual que en Transference2Example >>> /* Commit. */ connection.commit(); commited = true; connection.setTransactionIsolation( oldTransactionIsolation); connection.setAutoCommit(true); } finally { if (!commited) { connection.rollback(); } } } Comentarios n n No tiene problemas de concurrencia ¿ Qué ocurre exactamente ? n n n BDs con “Optimistic Locking” => si se ejecutan dos transferencias concurrentes del tipo transfer(connection, “fbellas-1”, “fbellas2”, 10), una de ellas gana, y la otra recibe una SQLException (no hay bloqueos y la BD queda en estado consistente) BDs con “Pessimistic Locking” => Mismo comportamiento que Transference3Example Realizar experimentos similares a Transference3Example Conclusiones acerca de concurrencia en transacciones (1) n Transacciones que leen de BD, realizan cálculos en memoria (con lo leído) y actualizan en BD (con lo calculado) n n En otro caso n n SELECT FOR UPDATE o TRANSACTION_SERIALIZABLE Véase transparencia “Transaction isolation levels (1)” Para adoptar un enfoque sencillo (y mantenible), bastante portable y seguro (no propenso a errores) n El código de los ejemplos de la asignatura usará TRANSACTION_SERIALIZABLE para todo tipo de transacciones Conclusiones acerca de concurrencia en transacciones (y 2) n En una aplicación claramente transaccional es mejor usar EJB (aunque usemos JDBC en la implementación de los EJBs) n n Las transacciones las gestiona el contenedor de EJBs El programador sólo necesita especificar qué métodos son transaccionales n n n n No hay que modificar el modo auto-commit de las conexiones ni cambiar el nivel de aislamiento EJB también implementa transacciones distribuidas EJB también permite automatizar la persistencia de objetos En resumen => más potencia + más portabilidad + más sencillez Otros temas n Scrollable ResultSets n n ResultSets actualizables n n Permiten hacer modificaciones a las filas correspondientes Procesamiento batch n n Permiten navegación sobre el ResultSet Permiten construir un Statement que agrupa a un conjunto de queries Soporte para tipos SQL3 n BLOB, CLOB, arrays, STRUCT, etc.