jueves, agosto 27, 2009

JDBC – Transacciones – Parte 3

El presente articulo, resulta ser el seguimiento de dos anteriores artículos, que usted puede encontrar en este mismo sitio Web; es importante que lea los anteriores artículos, con el fin que usted pueda comprender el código resultante, pues el código del actual articulo no es mas que una simple evolución del código analizado en artículos anteriores.

Que es una transacción?



Los que cursaron bases de datos en la Universidad deben tener bien claro, lo que el manejo de transacciones implica; diremos de forma muy general, que las transacciones nos sirven para agrupar un conjunto de sentencias SQL, regularmente de tipo “update” (insertar, eliminar o actualizar) bajo una misma operación, pudiendo rechazarla o enviarla toda ella a nuestro gusto. De esta forma podemos garantizar que todas las operaciones fueron ejecutadas como una sola operación atómica. Esto es muy importante, para mantener la consistencia; imaginemos la compra de un usuario, donde se guardan datos en diferentes tablas, tales como productos que compro, la información personal del usuario, la información del pago, la dirección de shipping; pero que pasaría si cobramos y guardamos esta información, de igual manera realizamos el cambio en nuestro inventario manifestando la venta, pero por alguna razón la dirección a donde hacer shipping no pudo ser guardada. Estaremos ante un caso de negocio, que nos deja mal parados ante el cliente que pago, pero no recibira sus productos, de igual manera hay muchos otros escenarios prácticos donde aplicar las transacciones.

Vamos al código; de forma similar al anterior articulo, donde vimos la implementación de un DAO con JDBC, en este capitulo introduciremos los patrones Factory y AbstractFactory, así como el Delegate; para implementar la fabrica de transacciones, la fabrica para crear las fabricas de transacciones y la clase que encapsula nuestras transacciones.

Lo primero que debemos definir, son las operaciones necesarias para realizar nuestro delegado, que será el que maneje nuestras transacciones:


/**
* Clase encargada de encapsular el manejo de transacciones, es decir se le delega
* la transaccionabilidad de las consultas.
* @author jsanca
*
*/
public interface TransactionDelegate {

/**
* Invocar a este metodo cuando inicia la transaccion.
* Deberia colocar todas las precondiciones necesarias para ejecutar la transaccion.
*/
public void start ();

/**
* Realiza el commit de la transaccion.
*/
public void commit ();

/**
* Realiza el rollback de la transaccion
*/
public void rollback ();

/**
* Invocar a este metodo para finalizar la transaccion, suceda o no un error.
* El mismo sera el encargado de volver al estado anterior a la transaccion
* y cerrar los recursos solicitados al inicio de la transaccion.
*/
public void end ();
} // E:O:F:TransactionDelegate.


Aunque los nombres de los método, no sean los mejores, me parece son bastante fáciles de deducir su funcionalidad, en todo caso imaginemos el siguiente escenario; contamos con 3 daos, los cuales realizan una sola transacción lógica, el flujo correcto para utilizar el delegado seria:


TransactionDelegate delegate = .. // código de inicialización

try {

delegate.start();

dao1.insert(...);
dao2.update(....);
dao2.delete(...)
dao3.update(..);
dao1.insert(...);

delegate.commit();
} catch (…) {

delegate.rollback();
} finally {

delegate.end();
}


Podemos concluir, que las transacciones, primero se inicializan, en caso que todas las operaciones sean exitosas, se realiza el commit, caso contrario rollback y siempre terminamos la transacción. Comentario aparte, merece mencionar el uso de programación orientada aspectos, para evitar todo este código dentro de un servicio de negocios, como se puede notar el código es bastante recetario y ensucia bastante la lectura del código.

Ahora que tenemos definido, que deseamos sea responsabilidad del Delegate, realicemos nuestra implementación para JDBC, no sin antes mencionar, que esta especificación nos permitiría manejar transacciones de forma transparente no solo para JDBC, si no para otros tipos de persistencia, pues el diseño por contrato que hemos hecho, evita el acopla con cualquier cosa relacionada con JDBC y solo especifica el concepto de transacción.


import java.sql.Connection;
import java.sql.SQLException;

/**
* Implementacion para JDBC
*
* @author jsanca
*
*/
public class JDBCTransactionDelegateImpl implements TransactionDelegate {

private Connection connection = null;

private boolean oldStateOfAutoCommit = false;
private int oldStateOfTransactionIsolation = Connection.TRANSACTION_READ_COMMITTED;

public JDBCTransactionDelegateImpl(Connection connection) {

this(connection, Connection.TRANSACTION_READ_COMMITTED);
} // JDBCTransactionDelegateImpl.

public JDBCTransactionDelegateImpl(Connection connection, int isolation) {

try {

this.connection = connection;
this.oldStateOfTransactionIsolation = this.connection
.getTransactionIsolation();
this.connection.setTransactionIsolation(isolation);
} catch (SQLException e) {

throw new DAOException(e);
}
} // JDBCTransactionDelegateImpl.

public void commit() {

try {

this.connection.commit();
} catch (SQLException e) {

throw new DAOException(e);
}
} // commit.

public void end() {

try {

this.connection.setAutoCommit(this.oldStateOfAutoCommit);
this.connection
.setTransactionIsolation(this.oldStateOfTransactionIsolation);
this.connection.close();
} catch (SQLException e) {

throw new DAOException(e);
}
} // end.

public void rollback() {

try {

this.connection.rollback();
} catch (SQLException e) {

throw new DAOException(e);
}
} // rollback.

public void start() {

try {

if (this.connection.getAutoCommit()) {
// En caso que la conexion se commite de forma
// automatica, guardamos el estado anterior e
// indicamos que no deseamos el commit automatico.
this.connection.setAutoCommit(false);
this.oldStateOfAutoCommit = true;
}
} catch (SQLException e) {

throw new DAOException(e);
}
} // start.

/**
* Este metodo solo es miscelaneo para los que quieran trabajar directamente
* con JDBC.
*
* @return Connection
*/
public Connection getConnection() {

return this.connection;
} // getConnection.

} // E:O:F:JDBCTransactionDelegateImpl.


Esta implementación es bastante simple, recibimos una conexión JDBC y en los métodos gestionamos su transaccionabilidad, solo quiero aclarar algunos puntos; en el método start, como puede apreciar guardamos el estado actual o por defecto de la transacción y configuramos la conexión para que evite el auto commit, esta propiedad no permite encadenar varias sentencias SQL bajo una misma transacción, pues envía al servidor directamente cada una de las operaciones, por este motivo debe ser seteado a false. También debemos poner nuestra atención en el transacción isolation; se preguntara que demonios es esto de la isolation, pues sucede que las transacciones pueden ser configuradas para comportarse de varias formas y todas ellas se realizan con base en el nivel de isolation, asignado, veamos:

Connection.TRANSACTION_NONE: con esta indicamos que no soportaremos transacciones.

Connection.TRANSACTION_READ_COMMITTED: este es el mejor valor para las transacciones, o al menos así lo percibo yo, pues no permite que otros usuarios puedan leer valores hasta que estos no se encuentren realmente enviados (commit) a la base de datos, así se evita la lectura de datos sucios.

Connection.TRANSACTION_READ_UNCOMMITED: todo lo contrario del anterior, podríamos leer un dato que aun no ha sido enviado de forma exitosa o inclusive un dato que este pronto a hacersele rollback.

Connection.TRANSACTION_REPEATABLE_READ: Este nivel de isolation, nos protege un poco trabajar con datos sucios, evita las lecturas repetidas, pero la lectura de datos fantasmas puede ocurrir.

Connection.TRANSACTION_SERIALIZABLE: Este es el nivel mas fuerte o seguro, pues evita todos los errores que permiten los niveles anteriores, el único problema es que todas estas validaciones para evitar errores, perjudicarían el perfomance de la aplicación, a continuación coloco de mas rápido a mas lentos los niveles antes citados:


TRANSACTION_NONE – FASTEST
TRANSACTION_READ_UNCOMMITED – FASTEST
TRANSACTION_READ_COMMITED – FAST
TRANSACTION_REPEATABLE_READ – MEDIUM
TRANSACTION_SERIALIZABLE – SLOW

Depende de las necesidades especificas del negocio, los ingenieros tendrán que decidir entre uno u otra.

Muy bien, continuemos con el ejemplo. Para encapsular en nuestros servicios de negocios, el tipo especifico de persistencia, debemos utilizar el delegado (la interface) y resultaría muy conveniente ocultar la creación de este mediante una fabrica, así tenemos la siguiente interface y su implementación:


/**
* Define el funcionamiento basico de una fabrica de transacciones.
* @author jonathansanchez
*
*/
public interface TransactionFactory {

/**
* Obtiene una transaccion.
* @return TransactionDelegate.
*/
public abstract TransactionDelegate createNewTransaction();

} // E:O:F:JDBCTransactionFactory.


import java.sql.Connection;
import java.sql.SQLException;

import javax.sql.DataSource;

/**
* Fabrica para crear transacciones
*
* @author jsanca
*
*/
public class JDBCTransactionFactoryImpl implements TransactionFactory {

private DataSource dataSource = null;

/**
* Setea el DataSource
*
* @param dataSource
*/
public void setDataSource(DataSource dataSource) {

this.dataSource = dataSource;
} // setDataSource.

/**
* Obtiene una conexion.
*
* @return Connection.
*/
public Connection getConnection() {

Connection connection = null;

try {

connection = this.dataSource.getConnection();
} catch (SQLException e) {

throw new DAOException(e);
}

return connection;
} // getConnection.

/* (non-Javadoc)
* @see com.jsanca.drivermanager.TransactionFactory#createNewTransaction()
*/
public TransactionDelegate createNewTransaction() {

Connection connection = null;
TransactionDelegate delegate = null;

connection = this.getConnection();
delegate = new JDBCTransactionDelegateImpl(connection);

return delegate;
} // createNewTransaction
} // E:O:F:JDBCTransactionFactory.


No hay mucho que decir de estas dos clases, simplemente encapsulan la creación del delegado y adicionalmente diremos que la implementación almacena un datasource (que es nuestro viejo pool de conexiones) para servir conexiones a la base de datos.

Llegados a este punto, necesitaremos hacer algunas modificaciones mínimas a nuestro BaseDAO y sus clases derivadas, intentaremos hacer los cambios mínimos con el fin de afectar, en el menor grado, las clases que ya pudieran existir:


import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.dbcp.BasicDataSource;

public class BaseDAO {

private static JDBCTransactionFactoryImpl transactionFactory = null;
private JDBCTransactionDelegateImpl delegate = null;
private boolean isTransactional = false;

/**
* Constructor por defecto.
*/
public BaseDAO() {

super();
} // BaseDAO.

/**
* Constructor por defecto.
*/
public BaseDAO(TransactionDelegate delegate) {

super();
this.delegate = JDBCTransactionDelegateImpl.class.cast(delegate);
this.isTransactional = true;
} // BaseDAO.

/**
* Este es un metodo utilitario, para crear un DataSource con base en los
* datos, contenidos en el properties.
* [Quiza no deberia estar aqui, pero se deja para
* evitar complicaciones]
*
* @param properties
*/
public static DataSource createDataSource(Properties properties) {

BasicDataSource dataSource = new BasicDataSource();
dataSource.setUrl(properties.getProperty("url"));
dataSource.setDriverClassName(properties.getProperty("driver"));
dataSource.setUsername(properties.getProperty("user"));
dataSource.setPassword(properties.getProperty("pass"));

return dataSource;
} // init.

protected Connection getConnection() {

return (null != this.delegate) ? this.delegate.getConnection()
: BaseDAO.transactionFactory.getConnection();
} // getConnection.

protected void closeQuiet(Connection conn) {

if (null != conn) {

try {
if (!this.isTransactional) {

conn.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
} // closeQuiet.

public static void setTransactionFactory(JDBCTransactionFactoryImpl transactionFactory) {

BaseDAO.transactionFactory = transactionFactory;
} // setTransactionFactory.

protected void closeQuiet(Statement stm) {

if (null != stm) {

try {
stm.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} // closeQuiet.

protected void closeQuiet(ResultSet rs) {

if (null != rs) {

try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} // closeQuiet.

protected void closeQuiet(Connection conn, Statement stm, ResultSet rs) {

this.closeQuiet(rs);
this.closeQuiet(stm);
this.closeQuiet(conn);
} // closeQuiet.

} // E:O:F:BaseDAO.


Como podrá darse cuenta, en realidad cambio poco; se agregaron dos constructores y el método getConnection, ahora obtiene la conexión de un TransaccionDelegate o directamente del TransaccionFactory, que guarda el pool de conexiones. En el caso del método closeQuit(Connection), primero preguntamos, si nos encontramos o no en una transacción, de esta forma evitamos cerrar la conexión actual que sera reutilizada en otros métodos o por otros daos.

Modifique sus DAO's, agregando los constructores de la super clase, en el caso del EmpleadoDAO seria:


public class EmpleadoDAOJDBCImpl extends BaseDAO implements EmpleadoDAO {


public EmpleadoDAOJDBCImpl() {
super();
}

public EmpleadoDAOJDBCImpl(TransactionDelegate delegate) {
super(delegate);
}
…...


Prontos a terminar este capitulo, implementaremos la fabrica abstracta, la cual es una fabrica de fabricas, que permite obtener implementaciones para diferentes tipos de persistencia, imaginemos que tuviéramos persistencias a través de Servicios Web, JDBC, JMS o alguna otra tecnología transaccional, la idea es que los servicios de negocios no se vean afectado por un cambio en el tipo de persistencia, para este propósito utilizar la fabrica abstracta nos cae de lujo:


import java.util.Properties;

import javax.sql.DataSource;

/**
* Fabrica para crear implementaciones de TransactionFactory.
* @author jonathansanchez
*
*/
public class TransactionFactoryAbstractFactory {

public final static String JDBC = "jdbc";

public final static String IMPLEMENTATION_TYPE_PROPERTY_KEY = "implementation";

public TransactionFactory createFactory (Properties factoryProperties) {

TransactionFactory factory = null;

if (JDBC.equalsIgnoreCase(factoryProperties.getProperty(IMPLEMENTATION_TYPE_PROPERTY_KEY))) {

factory = this.createFactory(factoryProperties);
}

return factory;
} // createFactory.

public JDBCTransactionFactoryImpl createJDBCFactory (Properties properties) {

DataSource dataSource = null;
JDBCTransactionFactoryImpl factory = null;

dataSource = BaseDAO.createDataSource(properties);
factory = new JDBCTransactionFactoryImpl ();
factory.setDataSource(dataSource);

return factory;
} // createJDBCFactory.
} // E:O:F:TransactionAbstractFactory.


Por ultimo modifiquemos nuestra prueba de unidad, para que este conforme a los nuevos cambios:


import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;

import junit.framework.TestCase;

public class EmpleadoDAOTest extends TestCase {

private EmpleadoDAO empleadoDAO = null;

private JDBCTransactionFactoryImpl factory = null;

private TransactionDelegate transactionDelegate = null;

public void setFactory(JDBCTransactionFactoryImpl factory) {
this.factory = factory;
}

private static SimpleDateFormat dateFormat = new SimpleDateFormat(
"dd/MM/yyyy");

public void testEmpleadoDAO() throws ParseException {

Integer id = null;
Map< string, object=""> empleado = new HashMap();
Map< string, object=""> empleado2;
Collection< map>> empleados;

empleado.put("name", "Bob Marley");
empleado.put("birth_date", dateFormat.parseObject("06/02/1945"));

try {

this.transactionDelegate.start();

id = this.empleadoDAO.insert(empleado);

assertNotNull(id);

empleado.put("empleado_id", id);

empleado2 = this.empleadoDAO.get(id);

assertNotNull(empleado2);

assertTrue(empleado, empleado2);

empleado.put("name", "Albert Einstein");
empleado.put("birth_date", dateFormat.parseObject("14/03/1879"));

assertTrue(this.empleadoDAO.update(empleado));

empleado2 = this.empleadoDAO.get(id);

assertNotNull(empleado2);

assertTrue(empleado, empleado2);

assertTrue(this.empleadoDAO.delete(id));

empleado2 = this.empleadoDAO.get(id);

if (null != empleado2) {

fail("El empleado debio ser nulo");
}

id = this.empleadoDAO.insert(empleado);

empleados = this.empleadoDAO.findAll();

assertNotNull(empleados);
assertTrue(empleados.size() > 0);

this.transactionDelegate.commit();
} catch (DAOException e) {

this.transactionDelegate.rollback();
throw e;
} finally {

this.transactionDelegate.end();
}
} // testCrud.

private void assertTrue(Map empleado,
Map empleado2) {

assertTrue(empleado.get("empleado_id").equals(
empleado2.get("empleado_id")));
assertTrue(empleado.get("name").equals(empleado2.get("name")));
assertTrue(empleado.get("birth_date").equals(
empleado2.get("birth_date")));

} // assertTrue.

@Override
protected void setUp() throws Exception {

super.setUp();
if (null == this.factory) {

Properties properties = new Properties();
TransactionFactoryAbstractFactory abstractFactory = new TransactionFactoryAbstractFactory();
properties.setProperty("url", "jdbc:postgresql://localhost:5432/mydatabase");
properties.setProperty("driver", "org.postgresql.Driver");
properties.setProperty("user", "myuser");
properties.setProperty("pass", "mypass");
properties
.setProperty(
TransactionFactoryAbstractFactory.IMPLEMENTATION_TYPE_PROPERTY_KEY,
TransactionFactoryAbstractFactory.JDBC);

this.factory = abstractFactory.createJDBCFactory(properties);
}

this.transactionDelegate = this.factory.createNewTransaction();

this.empleadoDAO = new EmpleadoDAOJDBCImpl(this.transactionDelegate);

} // setUp.
} // E:O:F:EmpleadoDAOTest.


La corremos y todo debería ir color verde :).

Podemos concluir que el uso de transacciones en Java resulta muy sencillo y hemos aprendido a encapsularlo de una manera bastante apropiada. Adicionalmente, seria perfecto que este código sea sacado de nuestro servicios de negocios y sea envuelto por objetos Proxy implementados a través de AOP, como lo mencionamos anteriormente, de esta manera ensuciaremos menos nuestros servicios lo que aumenta la cohesión y proporciona un código mas bonito, elegante y elegible.

Para capítulos posteriores, tocaremos temas como; lectura de cursores para paginación de resultados, ejecución de múltiples consultas en una sola operación (batch), cutpoints, el uso del RowSet, Wrapper para el resultSet, invocación a procedimientos almacenados, reflexión para popular nuestro Java Beans, entre otras cosas.

Un abrazo y espero les haya gustado.
J

No hay comentarios.: