Como parte de esta segunda entrega, vamos a introducirnos mas en el mundo de JDBC y entre otras cosas tales como Test Driven, Patrón de diseño DAO, Diseño por contrato, etc. Así también vamos a extender nuestro ejemplo anterior.
DAO y Diseño por contrato
Un objeto de acceso a datos (DAO por sus siglas en ingles) nos permite encapsular las operaciones de acceso a datos (a muy groso modo), si a este diseño le agregamos una programación por contrato, esto nos proporciona mucha agilidad en el proceso de programación; porque? Como veremos pronto, el diseño por contrato permite trabajar de forma independiente, pues no se ocupa contar con la implementación concreta para realizar algunas otras labores con anterioridad o en paralelo.
Nota: La explicación a fondo de los conceptos de DAO, diseño por contrato y Test driven sobrepasan los objetivos del presente articulo, por lo que no se profundizara en ellos, simplemente serán mostrados en el ejemplo.
En el articulo anterior, estuvimos trabajando con la tabla Empleado; una manera apropiada de acceder a los datos de esta entidad, es mediante la implementación de un patrón DAO, la misma será especificada mediante una interface la cual representa nuestro contrato a utilizar, independiente totalmente de la implementación final; vamos pues al ejemplo:
Si miramos con atención esta interface, simplemente estamos indicando que hacer, mas no el como. Las operaciones presentes en la interface, están relacionadas solamente con la gestión de la entidad y no con lógica de algún negocio en particular; en buena teoría podríamos tomar la entidad empleado y reutilizarla en algún otro proyecto.
Ya con esta interface, podríamos asignarle a un programador que implemente nuestra prueba de unidad para este DAO y a otro que vaya trabajando en la implementación del DAO.
A continuación mostraremos una versión muy simplificada de una posible prueba de unidad, para nuestro DAO:
Para los que están familiarizados con Junit, esta clase les será fácil de entender, no deseo entrar en detalles acerca del funcionamiento de la prueba de unidad, pero en términos generales invocamos a todas las operaciones que proporciona el DAO y determinamos si los resultados obtenidos, son los esperados, así bien preguntamos si el campo no es nulo, si el Mapa recuperado tiene los mismo valores que el obtenido de la base de datos, etc, si algunas de esta condiciones no se cumple, la prueba falla y muestra el respectivo error.
Ahora vamos a ir creando nuestra implementación, paso a paso:
Lo primero que vamos a crear, antes de nuestra implementación, será una excepción para reportar nuestro errores de acceso a datos, al trabajar con JDBC tendremos que gestionar excepciones derivadas del SQLException, sin embargo si cambiáramos de mecanismo de persistencia, estas excepciones ya no serian validas, por ejemplo que guardáramos todo en un archivo plano, probablemente tendríamos IOException en vez de SQLException, así que es mejor envolver (wrapper) las excepciones en una propia:
BaseDAO:
Una buena practica de programación, es encapsular ciertas operaciones comunes a varios objetos, en un objeto padre, para ello implementaremos un BaseDAO, el cual contenga algunas objetos o funcionalidad genérica:
Nuestro BaseDAO, solo gestiona nuestro pool de conexiones y proporciona algunos métodos para cerrar objetos JDBC sin enviar errores. Esto ultimo se realiza, pues en la mayoría de ocasiones si una conexión falla al cerrarse, es poco lo que podemos hacer, quizá podríamos indicarle al usuario del error, pero dependiendo del tipo de usuario, seria poco útil.
Método Get:
El método get obtiene un empleado, basado en el identificador pasado como parámetro a la función:
En este ejemplo, utilizamos el objeto PreparedStatement para realizar las consultas, este objeto como notaran, cuenta con varias diferencias al Statement, las cuales en su mayoría son ventajas; en el ejemplo anterior debíamos insertar los valores, concatenando las cadenas al query, con el PreparedStatement utilizamos los comodines ?. Estos comodines indican que espera un parámetro y el orden de aparición en el String, es el orden esperado a la hora de insertar los datos.
Así pues, para insertar el primer comodín una cadena tendríamos que codificar;
statement.setObject(1, “Una cadena”);
Tome en cuenta que el indice inicia en 1 y no en cero como en los vectores.
En nuestro ejemplo en particular, asignamos un entero que representa el identificador del empleado que deseamos recuperar.
Lo siguiente que debemos analizar es el método fillEmpleado; esta operación utiliza el ResultSet que ya habíamos visto, junto con un nuevo objeto, el ResultSetMetaData. El ResultSetMetaData contiene la meta información relacionada con la consulta, es decir el nombre de las columnas, cantidad de columnas recuperadas entre otras cosas. En nuestro ejemplo en particular, obtenemos el nombre de la columna y el valor asociado, para introducirlo en nuestro mapa con los datos correspondientes al empleado.
Lo ultimo que deseo ver, es la utilización de try, catch, finally; el try, permite comprobar excepciones, las excepciones comprobadas pueden ser gestionadas por la instrucción catch. Por ultimo, existan o no errores, las sentencias dentro de finally, serán siempre ejecutas, es por eso que debemos colocar aquí el código para cerrar nuestros recursos o cualquier otra operación que siempre deba ser ejecutada.
Método findAll:
Este procedimiento es muy parecido al método get, la única diferencia es que obtenemos una lista de mapas con los datos de los empleados, en vez de solo un mapa que representa a un solo empleado.
Método Delete:
Este método resulta muy parecido a los anteriores, los únicos puntos a tomar en cuenta es la invocación al método executeUpdate, en vez de executeQuery; el primero permite invocar operaciones de actualización, a.k.a: insert, update, delete. El mismo retorna un entero con la cantidad de filas afectadas por la operación.
Método Update:
Al igual que los métodos anteriores, este método simplemente asigna algunos datos mas (note que se utiliza java.sql.Date en vez de java.util.Date, pues es el dato que espera la BD en este caso particular), el resto del código debería resultarte ya entendible.
Método insert:
Antes de ver este código, quiero comentar una situación particular, que me ocurrió al programar el ejemplo; regularmente utilizamos la operación, executeUpdate para realizar la inserción, como en los casos anteriores, la única deferencia es que necesitamos obtener el identificador generado por la base de datos, algunos driver de JDBC no soportan este tipo de operaciones, por lo que se incluye un workaround (un hack o truco), para la inserción en el singular driver de Postgres; quiero dejar claro que no es la manera mas correcta, pero la incluyo pues puede ser útil para alguno otro mortal por ahí.
Bien, lo primero que deseo que noten, es la creación del PreparedStatement, después del query como segundo parámetro pasamos: Statement.RETURN_GENERATED_KEYS, esto le indica a nuestro driver que esperamos el identificador generado, similar a la operación update, asignamos los parámetros a insertar, ejecutamos la actualización y en caso que afecte mas de una fila, solicitamos un ResultSet del cual obtendríamos nuestro identificador generado. En caso que el codigo descrito anteriormente se desplome y arroje una excepción, esta capturada por la instrucción catch y analizada por el método isAutoGeneratedKeysIsNotSupported, en caso que el error describa algo relacionado con la generación de llaves, se intentara un método de insercion alternativo; altertiveInsert, este metodo ejecuta la siguiente operación, valida en Postgres: INSERT INTO empleado (name, birth_date) values (?, ?) RETURNING empleado.empleado_id, como notara el returning devolverá el identificador del empleado insertado, por ultimo tome en cuenta que la operación invocada es executeQuery en vez de executeUpdate, caso contrario obtendrá una excepciones pues la operación no espera nada de retorno.
Ahora que tenemos toda la implementación, solo nos basta volver a nuestra prueba de unidad e incluir el código para arrancar nuestro DAO.
Esta seria la versión definitiva y completa de nuestro test y nuestra implementación:
Bueno, es todo; espero haya sido de su agrado. Para futuros artículos comentaremos las operaciones en Batch, los cursores, transacciones, entre otros apasionantes temas.
Hasta la proxima!
DAO y Diseño por contrato
Un objeto de acceso a datos (DAO por sus siglas en ingles) nos permite encapsular las operaciones de acceso a datos (a muy groso modo), si a este diseño le agregamos una programación por contrato, esto nos proporciona mucha agilidad en el proceso de programación; porque? Como veremos pronto, el diseño por contrato permite trabajar de forma independiente, pues no se ocupa contar con la implementación concreta para realizar algunas otras labores con anterioridad o en paralelo.
Nota: La explicación a fondo de los conceptos de DAO, diseño por contrato y Test driven sobrepasan los objetivos del presente articulo, por lo que no se profundizara en ellos, simplemente serán mostrados en el ejemplo.
En el articulo anterior, estuvimos trabajando con la tabla Empleado; una manera apropiada de acceder a los datos de esta entidad, es mediante la implementación de un patrón DAO, la misma será especificada mediante una interface la cual representa nuestro contrato a utilizar, independiente totalmente de la implementación final; vamos pues al ejemplo:
import java.util.Collection;
import java.util.Map;
public interface EmpleadoDAO {
/**
* Retorna el empleado con el identificador id.
* @param id Integer
* @return Map
*/
public Mapget (Integer id);
/**
* Actualiza el empleado
* @param empleado Map
* @return boolean true si es exito
*/
public boolean update (Mapempleado);
/**
* Elimina el empleado
* @param id Integer
* @return boolean true si es exito.
*/
public boolean delete (Integer id);
/**
* Inserta el empleado
* @param empleado Mao
* @return Integer retorna el id del empleado nuevo.
*/
public Integer insert (Mapempleado);
/**
* Obtiene todos los empleados
* @return Collection con los empleados disponibles.
*/
public Collection
Si miramos con atención esta interface, simplemente estamos indicando que hacer, mas no el como. Las operaciones presentes en la interface, están relacionadas solamente con la gestión de la entidad y no con lógica de algún negocio en particular; en buena teoría podríamos tomar la entidad empleado y reutilizarla en algún otro proyecto.
Ya con esta interface, podríamos asignarle a un programador que implemente nuestra prueba de unidad para este DAO y a otro que vaya trabajando en la implementación del DAO.
A continuación mostraremos una versión muy simplificada de una posible prueba de unidad, para nuestro DAO:
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 static SimpleDateFormat dateFormat = new SimpleDateFormat(
"dd/MM/yyyy");
public void testEmpleadoDAO() throws ParseException {
Integer id = null;
Mapempleado = new HashMap ();
Mapempleado2;
Collection
Para los que están familiarizados con Junit, esta clase les será fácil de entender, no deseo entrar en detalles acerca del funcionamiento de la prueba de unidad, pero en términos generales invocamos a todas las operaciones que proporciona el DAO y determinamos si los resultados obtenidos, son los esperados, así bien preguntamos si el campo no es nulo, si el Mapa recuperado tiene los mismo valores que el obtenido de la base de datos, etc, si algunas de esta condiciones no se cumple, la prueba falla y muestra el respectivo error.
Ahora vamos a ir creando nuestra implementación, paso a paso:
Lo primero que vamos a crear, antes de nuestra implementación, será una excepción para reportar nuestro errores de acceso a datos, al trabajar con JDBC tendremos que gestionar excepciones derivadas del SQLException, sin embargo si cambiáramos de mecanismo de persistencia, estas excepciones ya no serian validas, por ejemplo que guardáramos todo en un archivo plano, probablemente tendríamos IOException en vez de SQLException, así que es mejor envolver (wrapper) las excepciones en una propia:
public class DAOException extends RuntimeException {
public DAOException() {
}
public DAOException(String arg0) {
super(arg0);
}
public DAOException(Throwable arg0) {
super(arg0);
}
public DAOException(String arg0, Throwable arg1) {
super(arg0, arg1);
}
}
BaseDAO:
Una buena practica de programación, es encapsular ciertas operaciones comunes a varios objetos, en un objeto padre, para ello implementaremos un BaseDAO, el cual contenga algunas objetos o funcionalidad genérica:
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 DataSource dataSource = null;
/**
* Inicializa los recursos comunes para todos los daos.
*
* @param properties
*/
public static void init(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"));
BaseDAO.dataSource = dataSource;
} // init.
protected Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
throw new DAOException(e);
}
} // getConnection.
protected void closeQuiet(Connection conn) {
if (null != conn) {
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
} // closeQuiet.
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.
Nuestro BaseDAO, solo gestiona nuestro pool de conexiones y proporciona algunos métodos para cerrar objetos JDBC sin enviar errores. Esto ultimo se realiza, pues en la mayoría de ocasiones si una conexión falla al cerrarse, es poco lo que podemos hacer, quizá podríamos indicarle al usuario del error, pero dependiendo del tipo de usuario, seria poco útil.
Método Get:
El método get obtiene un empleado, basado en el identificador pasado como parámetro a la función:
/*
* (non-Javadoc)
*
* @see com.jsanca.drivermanager.EmpleadoDAO#get(java.lang.Integer)
*/
public Mapget(Integer id) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
ResultSetMetaData metaData = null;
Mapempleado = null;
try {
connection = this.getConnection();
statement = connection
.prepareStatement("SELECT * FROM empleado WHERE empleado_id = ?;");
statement.setObject(1, id);
resultSet = statement.executeQuery();
metaData = resultSet.getMetaData();
if (resultSet.next()) {
empleado = this.fillEmpleado(resultSet, metaData);
}
} catch (SQLException e) {
throw new DAOException(e);
} finally {
this.closeQuiet(connection, statement, resultSet);
}
return empleado;
} // get.
private MapfillEmpleado(ResultSet resultSet,
ResultSetMetaData metaData) throws SQLException {
Mapempleado;
int columnCount;
String columnName;
empleado = new LinkedHashMap();
columnCount = metaData.getColumnCount();
for (int i = 1; i <= columnCount; i += 1) { columnName = metaData.getColumnName(i); empleado.put(columnName, resultSet.getObject(columnName)); } return empleado; } // fillEmpleado.
En este ejemplo, utilizamos el objeto PreparedStatement para realizar las consultas, este objeto como notaran, cuenta con varias diferencias al Statement, las cuales en su mayoría son ventajas; en el ejemplo anterior debíamos insertar los valores, concatenando las cadenas al query, con el PreparedStatement utilizamos los comodines ?. Estos comodines indican que espera un parámetro y el orden de aparición en el String, es el orden esperado a la hora de insertar los datos.
Así pues, para insertar el primer comodín una cadena tendríamos que codificar;
statement.setObject(1, “Una cadena”);
Tome en cuenta que el indice inicia en 1 y no en cero como en los vectores.
En nuestro ejemplo en particular, asignamos un entero que representa el identificador del empleado que deseamos recuperar.
Lo siguiente que debemos analizar es el método fillEmpleado; esta operación utiliza el ResultSet que ya habíamos visto, junto con un nuevo objeto, el ResultSetMetaData. El ResultSetMetaData contiene la meta información relacionada con la consulta, es decir el nombre de las columnas, cantidad de columnas recuperadas entre otras cosas. En nuestro ejemplo en particular, obtenemos el nombre de la columna y el valor asociado, para introducirlo en nuestro mapa con los datos correspondientes al empleado.
Lo ultimo que deseo ver, es la utilización de try, catch, finally; el try, permite comprobar excepciones, las excepciones comprobadas pueden ser gestionadas por la instrucción catch. Por ultimo, existan o no errores, las sentencias dentro de finally, serán siempre ejecutas, es por eso que debemos colocar aquí el código para cerrar nuestros recursos o cualquier otra operación que siempre deba ser ejecutada.
Método findAll:
public Collection
Este procedimiento es muy parecido al método get, la única diferencia es que obtenemos una lista de mapas con los datos de los empleados, en vez de solo un mapa que representa a un solo empleado.
Método Delete:
/*
* (non-Javadoc)
*
* @see com.jsanca.drivermanager.EmpleadoDAO#delete(java.lang.Integer)
*/
public boolean delete(Integer id) {
Connection connection = null;
PreparedStatement statement = null;
boolean success = false;
try {
connection = this.getConnection();
statement = connection
.prepareStatement("DELETE FROM empleado WHERE empleado_id = ?;");
statement.setObject(1, id);
success = statement.executeUpdate() > 0;
} catch (SQLException e) {
throw new DAOException(e);
} finally {
this.closeQuiet(statement);
this.closeQuiet(connection);
}
return success;
} // delete.
Este método resulta muy parecido a los anteriores, los únicos puntos a tomar en cuenta es la invocación al método executeUpdate, en vez de executeQuery; el primero permite invocar operaciones de actualización, a.k.a: insert, update, delete. El mismo retorna un entero con la cantidad de filas afectadas por la operación.
Método Update:
/*
* (non-Javadoc)
*
* @see com.jsanca.drivermanager.EmpleadoDAO#update(java.util.Map)
*/
public boolean update(Mapempleado) {
Connection connection = null;
PreparedStatement statement = null;
boolean success = false;
try {
connection = this.getConnection();
statement = connection
.prepareStatement("UPDATE empleado SET name = ?, birth_date = ? WHERE empleado_id = ?;");
this.setEmpleadoParameters(empleado, statement);
statement.setObject(3, empleado.get("empleado_id"));
success = statement.executeUpdate() > 0;
} catch (SQLException e) {
throw new DAOException(e);
} finally {
this.closeQuiet(statement);
this.closeQuiet(connection);
}
return success;
} // update.
private void setEmpleadoParameters(Mapempleado,
PreparedStatement statement) throws SQLException {
statement.setObject(1, empleado.get("name"));
statement.setDate(2, new java.sql.Date(Date.class.cast(
empleado.get("birth_date")).getTime()));
} // setEmpleadoParameters.
Al igual que los métodos anteriores, este método simplemente asigna algunos datos mas (note que se utiliza java.sql.Date en vez de java.util.Date, pues es el dato que espera la BD en este caso particular), el resto del código debería resultarte ya entendible.
Método insert:
Antes de ver este código, quiero comentar una situación particular, que me ocurrió al programar el ejemplo; regularmente utilizamos la operación, executeUpdate para realizar la inserción, como en los casos anteriores, la única deferencia es que necesitamos obtener el identificador generado por la base de datos, algunos driver de JDBC no soportan este tipo de operaciones, por lo que se incluye un workaround (un hack o truco), para la inserción en el singular driver de Postgres; quiero dejar claro que no es la manera mas correcta, pero la incluyo pues puede ser útil para alguno otro mortal por ahí.
/*
* (non-Javadoc)
*
* @see com.jsanca.drivermanager.EmpleadoDAO#insert(java.util.Map)
*/
public Integer insert(Mapempleado) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
Integer id = null;
try {
connection = this.getConnection();
statement = connection.prepareStatement(
"INSERT INTO empleado (name, birth_date) values (?, ?)",
Statement.RETURN_GENERATED_KEYS);
this.setEmpleadoParameters(empleado, statement);
if (statement.executeUpdate() > 0) {
resultSet = statement.getGeneratedKeys();
if (resultSet.next()) {
id = resultSet.getInt(1);
}
}
} catch (SQLException e) {
if (this.isAutoGeneratedKeysIsNotSupported(e)) {
id = altertiveInsert(empleado);
} else {
throw new DAOException(e);
}
} finally {
this.closeQuiet(statement);
this.closeQuiet(connection);
}
return id;
} // insert.
private Integer altertiveInsert(Mapempleado) {
Connection connection = null;
PreparedStatement statement = null;
ResultSet resultSet = null;
Integer id = null;
try {
connection = this.getConnection();
statement = connection
.prepareStatement("INSERT INTO empleado (name, birth_date) values (?, ?) RETURNING empleado.empleado_id");
this.setEmpleadoParameters(empleado, statement);
resultSet = statement.executeQuery();
if (resultSet.next()) {
id = Integer.class.cast(resultSet.getObject(1));
}
} catch (SQLException e) {
throw new DAOException(e);
} finally {
this.closeQuiet(statement);
this.closeQuiet(connection);
}
return id;
} // altertiveInsert.
private boolean isAutoGeneratedKeysIsNotSupported(SQLException e) {
boolean result = false;
result |= -1 != e.getMessage().indexOf("autogenerated");
result |= -1 != e.getMessage().indexOf("keys");
return result;
} // isAutoGeneratedKeysIsNotSupported.
Bien, lo primero que deseo que noten, es la creación del PreparedStatement, después del query como segundo parámetro pasamos: Statement.RETURN_GENERATED_KEYS, esto le indica a nuestro driver que esperamos el identificador generado, similar a la operación update, asignamos los parámetros a insertar, ejecutamos la actualización y en caso que afecte mas de una fila, solicitamos un ResultSet del cual obtendríamos nuestro identificador generado. En caso que el codigo descrito anteriormente se desplome y arroje una excepción, esta capturada por la instrucción catch y analizada por el método isAutoGeneratedKeysIsNotSupported, en caso que el error describa algo relacionado con la generación de llaves, se intentara un método de insercion alternativo; altertiveInsert, este metodo ejecuta la siguiente operación, valida en Postgres: INSERT INTO empleado (name, birth_date) values (?, ?) RETURNING empleado.empleado_id, como notara el returning devolverá el identificador del empleado insertado, por ultimo tome en cuenta que la operación invocada es executeQuery en vez de executeUpdate, caso contrario obtendrá una excepciones pues la operación no espera nada de retorno.
Ahora que tenemos toda la implementación, solo nos basta volver a nuestra prueba de unidad e incluir el código para arrancar nuestro DAO.
@Override
protected void setUp() throws Exception {
super.setUp();
Properties properties = new Properties ();
properties.setProperty("url", "jdbc:postgresql://localhost:5432/mydatabase");
properties.setProperty("driver", "org.postgresql.Driver");
properties.setProperty("user", "myuser");
properties.setProperty("pass", "mypass");
BaseDAO.init(properties);
this.empleadoDAO = new EmpleadoDAOJDBCImpl ();
} // setUp.
Esta seria la versión definitiva y completa de nuestro test y nuestra implementación:
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 static SimpleDateFormat dateFormat = new SimpleDateFormat(
"dd/MM/yyyy");
public void testEmpleadoDAO() throws ParseException {
Integer id = null;
Mapempleado = new HashMap ();
Mapempleado2;
Collection
Bueno, es todo; espero haya sido de su agrado. Para futuros artículos comentaremos las operaciones en Batch, los cursores, transacciones, entre otros apasionantes temas.
Hasta la proxima!
Comentarios