Apartado 2.1: Tutorial de JDBC

Anuncio
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.
Descargar