Está en la página 1de 71

2.

1 Tutorial de JDBC
Índice
n Introducción
n Accesos básicos
n Tipos SQL y Java
n DataSources
n Pool de conexiones
n Transacciones
n Otros temas
Introducción
n JDBC (Java DataBase Connectivity) es un API que permite
lanzar queries a una base de datos relacional
n Su diseño está inspirado en dos conocidas APIs
n ODBC (Open DataBase Connectivity)
n X/OPEN SQL CLI (Call Level Interface)
n El programador siempre trabaja contra los paquetes java.sql
y javax.sql
n java.sql: forma parte de J2SE
n javax.sql: forma parte de J2EE (hasta la versión 1.2), pero se
moverá a J2SE 1.4
n Contienen un buen número de interfaces y algunas clases
concretas, que conforman el API de JDBC
n Para poder conectarse a la BD y lanzar queries, es preciso tener
un driver adecuado para ella
n Un driver suele ser un fichero .jar que contiene una
implementación de todos los interfaces del API de JDBC
n 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 javax.sql

<<access>> Driver JDBC


BD
Tipos de drivers (1)

Driver Tipo 1 <<use>> API nativa


(ej.: bridge estándar
JDBC-OBDC) (ej.: OBDC)

<<access>>

Driver Tipo 2 <<use>> API nativa BD


(ej: Oracle OCI) (normalmente en <<access>>
C/C++)

<<access>> BD

Driver Tipo 3 <<use>> Servidor con API


genérica

Driver Tipo 4
(ej: Oracle thin) <<access>>
Tipos de drivers (y 2)
n Comentarios
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)
n Los drivers de tipo 3 y 4 son drivers 100% Java
n La ventaja de un driver de tipo 3 es que una aplicación
puede usarlo para conectarse a varias BDs
n Pero esto también se puede conseguir usando un driver distinto
(de los otros tipos) para cada BD
n Eficiencia: en general, los drivers más eficientes son los de
tipo 2
Independencia de la BD
n Idealmente, si nuestra aplicación cambia de BD, no
necesitamos cambiar el código; simplemente,
necesitamos otro driver
n Sin embargo, desafortunadamente las BDs
relacionales usan distintos dialectos de SQL (¡ a pesar
de que en teoría es un estándar !)
n Tipos de datos: varían mucho según la BD
n Generación de identificadores: secuencias, autonumerados,
etc.
n 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
Alternativas a JDBC
n SQLJ
n Solución similar a la existente en otros lenguajes, que
consiste en insertar sentencias SQL dentro del código Java
n Posteriormente un preprocesador generará código
(seguramente usando JDBC)
n ¡ Alternativa fea (no orientada a objetos) !
n Mapeadores Objeto-Relacional
n Permiten almacenar, recuperar y buscar en BD instancias de
clases Java
n Ej.: JavaBlend de Sun
n Las consultas no suelen ser eficientes (excepto que sean
muy sencillas)
n Bases de datos orientadas a objetos
n Todavía no tienen tanta aceptación como las relacionales
(más eficientes)
Ejemplo de actualización: es.udc.fbellas.j2ee.jdbctutorial.InsertExample (1)

package es.udc.fbellas.j2ee.jdbctutorial;

import java.sql.Connection;
import java.sql.Statement;
Import java.sql.DriverManager;
import java.sql.SQLException;

public final class InsertExample {


public static void main (String[] args) {

Connection connection = null;


Statement statement = null;

try {
/* Get a connection. */
String driverClassName = "org.postgresql.Driver";
String driverUrl="jdbc:postgresql://localhost/fbellas";
String user = "fbellas";
String password = "";

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};

/* Insert the accounts in database. */


for (int i=0; i<accountIdentifiers.length; i++) {

/* Create "statement". */
statement = connection.createStatement();

/* 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 Su inicializador static registra el driver en
DriverManager
n En una aplicación real
n El nombre de la clase del driver, la URL (depende del
driver), el nombre de usuario y la password deberían ser
configurables (ej.: leerlos de un fichero de configuración)
n Existe una alternativa mejor a DriverManager
(javax.sql.DataSource)
n 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 eliminado de memoria el bloque anterior)
Comentarios (2)

+manages 0..n <<Interface>>


DriverManager
Driver

<<instantiate>>

<<Interface>> <<instantiate>> <<Interface>> <<instantiate>> <<Interface>>


Connection Statement ResultSet
Comentarios (y 3)
n Liberación de recursos
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
n Además
n Cuando se cierra una conexión, cierra todos sus Statements
asociados
n Cuando se cierra un Statement, cierra todos sus
ResultSets asociados
n Sin embargo,
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
n 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 La parsea
n Determina qué se quiere hacer
n Determina cómo ejecutarla
n Ejemplo InsertExample
n Existe un bucle en el que repetidamente se lanza la misma
query INSERT con distintos parámetros
n ¡ La BD construye un plan para ejecutar la misma query
cada vez !
n En este tipo de situaciones, es mejor usar
PreparedStatement
n PreparedStatement
n Es un interfaz que deriva de Statement
n 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)

/* Insert the accounts in database. */


for (int i=0; i<accountIdentifiers.length; i++) {

/* Create "preparedStatement". */
String queryString = "INSERT INTO TutAccount " +
"(accId, balance) VALUES (?, ?)";
preparedStatement =
connection.prepareStatement(queryString);

/* 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 PreparedStatement es más eficiente en bucles
que lanzan la misma query con distintos parámetros
n Statement obliga a formatear los datos de la query
porque ésta se envía a la BD tal cual la escribimos
n Las cadenas de caracteres tienen que ir entre ‘’ => código
menos legible y más propenso a errores
n Existen algunos tipos de datos (ej.: fechas) que se
especifican de manera distinta según la BD
n Con PreparedStatement el formateo de los datos
lo hace el driver
n Conclusión
n En general, usar siempre PreparedStatement
Tipos SQL y Java
n ResultSet y PreparedStatement proporcionan
métodos getXXX y setXXX
n ¿ Cuál es la correspondencia entre tipos Java y tipos SQL ?
n Idea básica: un dato de tipo Java se puede almacenar en
una columna cuyo tipo SQL sea consistente con el tipo Java
n 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 Tipo SQL
boolean 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 Conocer el nombre de la clase del driver
n La cadena de conexión
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
n Interfaz javax.sql.DataSource
n Es la alternativa recomendada a
DriverManager.getConnection
n No hay que escribir código para registrar el driver
n Actúa como factoría de conexiones
DataSources (2)
n Normalmente una aplicación obtiene una referencia a
un objeto DataSource mediante JNDI
n JNDI (Java Naming and Directory Interface)
n Familia de paquetes javax.naming
n Formó parte de J2EE (hasta versión 1.2), pero se movió a
J2SE (desde la versión 1.3)
n Es un API que proporciona un conjunto de interfaces para
acceder a un servicio de nombres y directorios
n Servicio de nombres y directorios
n Permite gestionar información de manera jerárquica
n Usuarios (login+password, directorios home, etc.), máquinas
de la red (direcciones ethernet e IP, etc.)
n 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 LDAP (Lightweight Directory Access Protocol)
n NIS/NIS+ (Network Information System)
n Microsoft Active Directory
n JNDI permite acceder a cualquier servicio de
nombres y directorios usando la misma interfaz
n Se necesita disponer de una implementación de JNDI para el
servicio en cuestión
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/fbellas”);

Connection connection = dataSource.getConnection();

n ¿ Quién registra los objetos DataSource en el


servicio de nombres y directorios ?
n Podría hacerlo el programador usando JDNI
n 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 A partir del tema 4 (tecnologías web) usaremos JNDI


o una solución similar para localizar objetos
DataSource
n Mientras tanto ...
<<Interface>>
DataSource
(from sql)

SimpleDataSource
ConfigurationParametersManager
<<use>>
(from configuration)
+ SimpleDataSource()

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 ConfigurationParameters.properties tiene que estar
en el CLASSPATH
n Ejemplo de especificación de parámetros de configuración en
ConfigurationParameters.properties

# ------------------------------------------------------------
# SimpleDataSource.
# ------------------------------------------------------------

SimpleDataSource/driverClassName=org.postgresql.Driver
SimpleDataSource/url=jdbc:postgresql://localhost/fbellas
SimpleDataSource/user=fbellas
SimpleDataSource/password=
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 Se solicitan muchas conexiones a la BD por minuto
n Pedir una conexión cada vez se convierte en un cuello de botella
n Solución: pool de conexiones
n Ejemplo de implementación

<<instantiate>>
<<interface>> <<interface>>
java.sql.Connection javax.sql.DataSource

ConnectionPool
ConnectionWrapper
<<instantiate>>
- connections: java.util.LinkedList
- c : java.sql.Connection
releaseConnection(c:Connection):void
Pool de conexiones (2)
n Ejemplo de implementación (cont)
n ConnectionPool
n Una sóla instancia
n Cuando se crea, pide “n” conexiones a la BD (quizás usando
DriverManager.getConnection) y las almacena en una lista
n getConnection
n Si quedan conexiones en la lista, devuelve una de ellas dentro de un objeto
ConnectionWrapper (comprobando antes si está “viva”)
n En otro caso, deja durmiendo al thread llamador usando wait
n releaseConnection (con visibilidad “package”)
n Devuelve la conexión a la lista, y notifica (notifyAll) a los posibles
threads que esperan por una conexión
n ConnectionWrapper
n Encapsula a una conexión real
n close
n Usa releaseConnection para devolver la conexión real al pool
n finalize
n Si no se ha llamado a ConnectionWraper.close, lo llama
n Resto de operaciones
n Delegan en la conexión real
Pool de conexiones (y 3)
n Ejemplo de implementación (cont)
n El programador trabaja contra javax.sql.DataSource y
java.sql.Connection
n Su código no sabe que está trabajando contra un pool de
conexiones
n 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 Permiten ejecutar bloques de código con las
propiedades ACID (Atomicity-Consistency-Isolation-
Durability)
n Por defecto, cuando se crea una conexión está en
modo auto-commit
n Cada Statement y PreparedStatement que se ejecute
sobre ella irá en su propia transacción
n Para ejecutar varias queries en una misma
transacción es preciso
n Deshabilitar el modo auto-commit de la conexión
n Lanzar las queries
n 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 el
bucle de creació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);

<< Bucle para 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 Propiedad “I” en ACID
n Para el programador es como si la BD serializase las
transacciones
n En realidad, dependiendo de la estrategia de bloqueo
que se use, pueden darse los siguientes problemas
n Dirty reads
n Non-repeatable reads
n 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 begin
pero no comprometido */
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 TRANSACTION_NONE: transacciones no soportadas
n TRANSACTION_READ_UNCOMMITED: pueden ocurrir “dirty reads”,
“un-repeatable reads” y “phantom reads”
n TRANSACTION_READ_COMMITED: pueden ocurrir “un-repeatable
reads” y “phantom reads”
n TRANSACTION_REPEATABLE_READ: pueden ocurrir “phantom
reads”
n TRANSACTION_SERIALIZABLE: elimina todos los problemas de
concurrencia
n 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 Suelen soportar TRANSACTION_READ_COMMITED y
TRANSACTION_SERIALIZABLE
n ¿ Cuál es el nivel de aislamiento por defecto
en las conexiones ?
n Depende del driver/BD
n Suele ser TRANSACTION_READ_COMMITED
Una transferencia bancaria
n Ejemplos
es.udc.fbellas.j2ee.jdbctutorial.Transf
erence*Example
n Realizan una transferencia de dinero entre dos cuentas
bancarias
n Veremos 4 estrategias
n Objetivo
n Estudiar los problemas de concurrencia que puede tener una
transacción típica que
n Lee un dato (o más) de BD
n Lo actualizar en memoria (ej.: le suma una cantidad)
n Lo actualiza en BD
n 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 Asumiendo conexión con TRANSACTION_READ_COMMITED
(que es lo normal) o superior => no hay problemas de
concurrencia
n 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
n Si se viola => el primer UPDATE provocará una SQLException
n Problemas:
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 ?
n ¿ 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


InsufficientBalanceExcepton o similares en vez de los
SQLException(...)
Comentarios (1)
n La solución es más genérica
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
n Problema
n Si la conexión tiene un nivel inferior a
TRANSACTION_SERIALIZABLE => problemas de concurrencia
n Ejemplo
n Dos transacciones ejecutando transfer(connection,
“fbellas-1”, “fbellas-2”, 10)
n Supongamos inicialmente => (fbellas-1, 100) y (fbellas-
2, 200)
Comentarios (2)
Transacción 1 Transacción 2
Leer balances => Leer balances =>
sourceAccountBalance = 100 sourceAccountBalance = 100
destinationAccountBalance = 200 destinationAccountBalance = 200

Calcular nuevos balances => Calcular nuevos balances =>


sourceAccountNewBalance = 90 sourceAccountNewBalance = 90
destinationAccountNewBalance = 210 destinationAccountNewBalance = 210

Actualizar cuentas =>


fbellas-1 => balance = 90
fbellas-2 => balance = 210
Actualizar cuentas =>
fbellas-1 => balance = 90
fbellas-2 => balance = 210

n Problema: hemos perdido los efectos de una de las transacciones


n 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 .. y la implementación de transfer usa
getAmount(accountIdentifier, true)
n No tiene problemas de concurrencia
n SELECT FOR UPDATE bloque la fila (filas) en cuestión
hasta que termina la transacción
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
n Si hubiese dos transferencias concurrentes del estilo ..
n 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)
Comentarios (y 2)
n Experimentar ejecutando ...
java –classpath J2EE_EXAMPLES_CLASSPATH
es.udc.fbellas.j2ee.jdbctutorial.Transference3Example
fbellas-1 fbellas-2

... desde dos ventanas


n ... 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. */
connection.setAutoCommit(false);
int oldTransactionIsolation =
connection.getTransactionIsolation();
connection.setTransactionIsolation(
Connection.TRANSACTION_SERIALIZABLE);

<< Igual que en Transference2Example >>>


es.udc.fbellas.j2ee.jdbctutorial.Transference4Example (y 2)

/* Commit. */
connection.commit();
commited = true;
connection.setTransactionIsolation(
oldTransactionIsolation);
connection.setAutoCommit(true);

} finally {
if (!commited) {
connection.rollback();
}
}

}
Comentarios
n No tiene problemas de concurrencia
n ¿ Qué ocurre exactamente ?
n BDs con “Optimistic Locking” => si se ejecutan dos
transferencias concurrentes del tipo
transfer(connection, “fbellas-1”, “fbellas-
2”, 10), una de ellas gana, y la otra recibe una
SQLException (no hay bloqueos y la BD queda en estado
consistente)
n BDs con “Pessimistic Locking” => Mismo comportamiento
que Transference3Example
n Realizar experimentos similares a
Transference3Example
Conclusiones acerca de concurrencia en transacciones (1)

n Transacciones que sólo insertan filas o actualizan sin


leer en memoria
n Es suficiente con TRANSACTION_READ_COMMITED
n Transacciones que releen una fila o un conjunto de
filas
n TRANSACTION_REPEATABLE_READ o
TRANSACTION_SERIALIZABLE, respectivamente
n Transacciones que leen de BD, realizan cálculos en
memoria (con lo leído) y actualizan en BD (con lo
calculado)
n SELECT FOR UPDATE o TRANSACTION_SERIALIZABLE
n Advertencia: algunas BDs podrían comportarse de
una manera ligeramente distinta a lo descrito
n En particular, Oracle y PostgreSQL funcionan como se ha
descrito (con “Optimistic Locking”)
Conclusiones acerca de concurrencia en transacciones (y 2)

n 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
n En una aplicación claramente transaccional es mejor usar EJB
(aunque usemos JDBC en la implementación de los EJBs)
n Las transacciones las gestiona el contenedor de EJBs
n El programador sólo necesita especificar qué métodos son
transaccionales
n No hay modificar el modo auto-commit de las conexiones ni cambiar el
nivel de aislamiento
n EJB también implementa transacciones distribuidas
n EJB también permite automatizar la persistencia de objetos
n En resumen => más potencia + más portabilidad + más sencillez
Otros temas
n Scrollable ResultSets
n Permiten navegación sobre el ResultSet
n ResultSets actualizables
n Permiten hacer modificaciones a las filas correspondientes
n Procesamiento batch
n Permiten construir un Statement que agrupa a un
conjunto de queries
n Soporte para tipos SQL3
n BLOB, CLOB, arrays, STRUCT, etc.

También podría gustarte