Proyecto de Integración
 

Acceso a la base de datos

Introducción

El objetivo de esta sesión de integración será implementar la capa de acceso a datos de nuestra aplicación, para lo que utilizaremos el patrón DAO. Dado que el acceso a datos es un elemento crítico, sobre el que se apoyarán el resto de capas de la aplicación, y que además es probable que sufra cambios en el futuro, deberemos definir esta capa de forma que los posibles cambios en la implementación del acceso a datos no afecten a las capas superiores. Para ello haremos uso de todos aquellos elementos y patrones que nos permitan aislar a quien use nuestra capa de acceso a datos de su implementació concreta (interfaces, factoría, nested exceptions).

Preparación del entorno

Implementaremos el módulo de acceso a datos como un nuevo proyecto. Por lo tanto, deberemos crear dicho proyecto con Maven utilizando el siguiente comando:

$ cd /home/especialista/proy-int
$ mvn archetype:generate 
-DarchetypeArtifactId=maven-archetype-quickstart 
-DgroupId=org.especialistajee.proyint -DartifactId=jbib-persist-dao 
-Dversion=1.0-SNAPSHOT -Dpackage=org.especialistajee.jbib.dao

Una vez creado el proyecto Maven, lo importaremos en Eclipse (Import ... > Maven > Existing Maven Projects). Podemos borrar las clases autogeneradas por Maven (App y AppTest). Además, nos aseguraremos de que se esté compilando con Java 1.6. Para eso pulsamos con el botón derecho sobre el proyecto, entramos en Properties > Java Compiler y comprobamos que Compiler compliance level esté puesto a Java 1.6. Si esto no es así, podemos desmarcar Enable project specific settings para que por defecto coja nivel 1.6, que es lo que deberíamos tener configurado a nivel global en Eclipse.

Tras importar el proyecto, insertaremos en él los scripts para crear e inicializar la base de datos. El primero de ellos irá en el directorio src/main/sql/biblioteca.sql, y el segundo en src/test/sql/datos.sql.

También introduciremos los ficheros para configurar log4j mediante commons-logging en src/main/resources. Si esta carpeta no existe, deberemos crearla manualmente. Tras hacer esto, para que sea reconocida como carpeta de fuentes es posible que sea necesario ejecutar Maven > Update Project Configuration.

Deberemos realizar una serie de modificaciones el fichero POM generado automáticamente, que explicaremos a continuación. El resultado final sería pom.xml.

Dado que vamos a acceder a una base de datos MySQL mediante JDBC, necesitaremos tener una dependencia al driver correspondiente en el fichero POM.

<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
  <version>5.0.8</version>
  <type>jar</type>
  <scope>compile</scope>
</dependency>

También vamos a incluir en nuestro proyecto la librería log4j, a la que accederemos a través de la librería commons-logging para así poder cambiar fácilmente en el futuro a otra librería de logs. Por lo tanto, tendremos dependencias a estas dos librerías.

<dependency>
  <groupId>commons-logging</groupId>
  <artifactId>commons-logging</artifactId>
  <version>1.1.1</version>
  <type>jar</type>
  <scope>compile</scope>
</dependency>
<dependency>
  <groupId>log4j</groupId>
  <artifactId>log4j</artifactId>
  <version>1.2.14</version>
  <type>jar</type>
  <scope>compile</scope>
</dependency>

Queremos poder probar nuestro módulo con JUnit, por lo que también deberá existir una dependencia a dicha librería.

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.8.1</version>
  <type>jar</type>
  <scope>test</scope>
</dependency>

Además, este proyecto deberá depender también del proyecto común creado en la sesión anterior:

<dependency>
  <groupId>org.especialistajee.proyint</groupId>
  <artifactId>jbib-comun</artifactId>
  <version>1.0</version>
  <type>jar</type>
  <scope>compile</scope>
</dependency>

Además, al realizar las pruebas nos interesará que la base de datos se cree y se inicialice en nuestro entorno de desarrollo. Para poder hacer esto automáticamente, introduciremos el plugin SQL en el fichero POM.

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>sql-maven-plugin</artifactId>
  <version>1.4</version>

  <dependencies>
    <!-- specify the dependent JDBC driver here -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.0.8</version>
    </dependency>
  </dependencies>

  <!-- common configuration shared by all executions -->
  <configuration>
    <driver>com.mysql.jdbc.Driver</driver>
    <url>jdbc:mysql://localhost:3306/</url>
    <username>root</username>
    <password>especialista</password>
  </configuration>
        
  ...

Indicaremos que la base de datos se inicialice siempre antes de realizar las pruebas (fase process-test-resources).

  ...

  <executions>
    <execution>
      <id>create-db</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>execute</goal>
      </goals>
      <configuration>              
        <srcFiles>
          <srcFile>src/main/sql/biblioteca.sql</srcFile>
        </srcFiles>
      </configuration>
    </execution>
          
    <execution>
      <id>create-data</id>
      <phase>process-test-resources</phase>
      <goals>
        <goal>execute</goal>
      </goals>
      <configuration>              
        <srcFiles>
          <srcFile>src/test/sql/datos.sql</srcFile>
        </srcFiles>
      </configuration>
    </execution>
  </executions>
</plugin>

Al guardar el fichero POM con estos cambios veremos como ejecuta de forma automática las sentencias SQL de nuestros scripts.

Operaciones de acceso a datos

En primer lugar deberemos decidir la lista de operaciones que ofrecerán nuestros DAOs. Tras un minucioso análisis de los requerimientos funcionales de nuestra aplicación, determinamos que las operaciones de acceso a datos que necesitaremos para implementar estas funcionalidades son las siguientes:

  • Libros (operaciones CRUD)
    • addLibro(libro): Crea un libro.
    • delLibro(isbn): Elimina un libro.
    • updateLibro(libro): Actualiza un libro.
    • getLibro(isbn): Obtiene los datos de un libro.
    • getLibros(): Obtiene el listado de todos los libros.
  • Usuarios (operaciones CRUD)
    • addUsuario(usuario): Crea un usuario.
    • delUsuario(login): Borra un usuario.
    • updateUsuario(usuario): Actualiza un usuario.
    • getUsuario(login): Obtiene los datos de un usuario.
    • getUsuarios(): Obtiene el listado de todos los usuarios.
  • Multas (alta, borrado y listados)
    • addMulta(multa): Crea una multa.
    • delMulta(idMulta): Borra una multa.
    • getMultasActivas(login): Obtiene la lista de multas activas para un usuario dado.
    • getUsuariosMorosos(): Obtiene el listado de usuarios morosos.
  • Operaciones (operaciones CRUD, listados y comprobaciones)
    • addOperacion(operacion): Crea una operación
    • delOperacion(idOperacion): Borra una operación.
    • updateOperacion(operacion): Actualiza una operación.
    • getOperacion(idOperacion): Obtiene los datos de una operación.
    • getOperaciones(tipoOperacion, estadoOperacion): Obtiene todas las operaciones de un tipo (préstamo, reserva o depósito) y estado dados (activas o históricas).
    • getOperacionesUsuario(login, estadoOperacion): Obtiene todas las operaciones de un usuario y estado dados (activas o históricas).
    • getOperacionesLibro(isbn, estadoOperacion): Obtiene todas las operaciones de un libro y estado dados (activas o históricas).
    • getLibrosDisponibles(): Obtiene el listado de libros disponibles para reserva, préstamo o depósito (es decir, libros sin ninguna operación activa asociada).

Es posible que cualquiera de estas operaciones produzca algún fallo, por ejemplo si el servidor de base de datos no se encuentra activo. Por lo tanto, deberemos tratar estos posibles errores mediante excepciones. Utilizaremos una excepción propia para nuestro DAO, a la que llamaremos DaoException, que heredará de la excepción base de nuestra aplicación (BibliotecaException).

El DAO estará en un paquete org.especialistajee.jbib.dao. Por lo tanto, será en este paquete en el que ubicaremos la excepción para nuestro DAO. Crearemos en primer lugar esta excepción:

/**
  * $Author$
  * $Date$
  * $Revision$
  */
package org.especialistajee.jbib.dao;
import org.especialistajee.jbib.BibliotecaException;

/**
 * Manejo de excepciones para los Dao
 */
public class DaoException extends BibliotecaException {

  private static final long serialVersionUID=6158132279964132104L;

  /**
   * Constructor de la clase 
   */
  public DaoException() {
    super();
  }

  /**
   * Constructor de la clase con mensaje
   * @param message Mensaje con la excepcion
   */
  public DaoException(String message) {
    super(message);
  }

  /**
   * Constructor de la clase con mensaje y causa
   * @param message Mensaje con la excepcion
   * @param cause Causa de la excepcion
   */
  public DaoException(String message, Throwable cause) {
    super(message, cause);
  }
}

De esta forma, nuestro DAO siempre devolverá DaoException, independientemente de la implementación concreta que estemos utilizando para acceder a los datos. Además, podemos ver que ofrecemos constructores de esta excepción a los que se proporciona como parámetro la excepción causante, con lo que podremos utilizar nested exceptions.

Interfaces para el DAO

Ahora deberemos crear las interfaces de cada DAO de nuestro sistema, con las operaciones que hemos determinado para cada uno de ellos.

Crearemos en primer lugar la interfaz para acceder al DAO de libros (ILibroDao):

package org.especialistajee.jbib.dao;
import java.util.List;
import org.especialistajee.jbib.model.LibroDomain;

/**
 * Interface para el DAO Libro
 *
 * @author $Author$
 * @version $Revision$
 */
public interface ILibroDao {

  /**
   * Obtiene los datos de un libro de la BD
   * @param isbn Isbn del libro a consultar
   * @return Libro seleccionado
   * @throws DaoException
   */
  LibroDomain getLibro(String isbn) throws DaoException;

  /**
   * Añade un libro a la BD
   * @param libro Libro a añadir.
   * @throws DaoException
   */
  void addLibro(LibroDomain libro) throws DaoException;
  
  /**
   * El libro pasado por parámetro es borrado de la BD
   * @param isbn Libro a eliminar
   * @return Número de registros afectados por el borrado
   * @throws DaoException
   */
  int delLibro(String isbn) throws DaoException;
    
  /**
   * Devuelve una lista con todos los libros almacenados en la BD
   * @return Lista con todos los libros
   * @throws DaoException
   */
  List<LibroDomain> getLibros() throws DaoException;
  
  /**
   * Actualiza el valor de un libro en la BD
   * @param libro Libro a modificar.
   * @return Numero de libros actualizados
   * @throws DaoException
   */
  int updateLibro(LibroDomain libro) throws DaoException;
}
Nota
En las siguientes interfaces se ha eliminado la documentación Javadoc para simplificar. Podríais añadirla vosotros de forma opcional tomando como ejemplo la de la interfaz anterior.

A continuación crearemos la interfaz IUsuarioDao para dar acceso a los usuarios:

package org.especialistajee.jbib.dao;
import java.util.List;
import org.especialistajee.jbib.model.UsuarioDomain;

public interface IUsuarioDao {
  UsuarioDomain getUsuario(String login) throws DaoException;
  int updateUsuario(UsuarioDomain usuario) throws DaoException;
  void addUsuario(UsuarioDomain usuario) throws DaoException;
  List<UsuarioDomain> getUsuarios() throws DaoException;
  int delUsuario(String login) throws DaoException;
}

También necesitaremos la interfaz IMultaDao, para acceder a la información sobre las multas:

package org.especialistajee.jbib.dao;
import java.util.List;
import org.especialistajee.jbib.model.MultaDomain;

public interface IMultaDao {
  int addMulta(MultaDomain multa) throws DaoException;
  int delMulta(int multaId) throws DaoException;
  List<MultaDomain> getMultasActivas(String login) 
                                               throws DaoException;
  List<MultaDomain> getUsuariosMorosos() throws DaoException;
}

Por último, introduciremos la interfaz IOperacionDao, para acceder a los datos sobre las operaciones:

package org.especialistajee.jbib.dao;

import java.util.List;

import org.especialistajee.jbib.model.EstadoOperacion;
import org.especialistajee.jbib.model.LibroDomain;
import org.especialistajee.jbib.model.OperacionDomain;
import org.especialistajee.jbib.model.TipoOperacion;

public interface IOperacionDao {
  OperacionDomain getOperacion(String idOperacion) 
	        throws DaoException;
  int updateOperacion(OperacionDomain operacion) 
            throws DaoException;
  String addOperacion(OperacionDomain operacion) 
            throws DaoException;
  int delOperacion(String idOperacion) throws DaoException;
  List<OperacionDomain> getOperaciones(TipoOperacion tipo,
            EstadoOperacion estado) throws DaoException;
  List<OperacionDomain> getOperacionesUsuario(String login,
            EstadoOperacion estado) throws DaoException;
  List<OperacionDomain> getOperacionesLibro(String isbn,
            EstadoOperacion estado) throws DaoException;
  List<LibroDomain> getLibrosDisponibles() 
            throws DaoException;
}
Nota
Por el momento hemos determinado que estas serán las operaciones de acceso a datos que necesitará nuestra aplicación. Sin embargo, puede que más adelante durante el desarrollo de la misma necesitemos añadir nuevas operaciones a estos DAOs.

Factorías de DAOs

Para independizar al resto de capas de nuestra aplicación de la instanciación de la implementación concreta del DAO que se vaya a utilizar, realizaremos esta instanciación siempre mediante una factoría. Para especificar las distintas formas de acceso a datos que soporta nuestra aplicación utilizaremos una enumeración:

package org.especialistajee.jbib.dao;

public enum TipoDaos {
  JDBC_MYSQL, AMAZON_SIMPLEDB, GOOGLE_BIGTABLE;
}

Utilizaremos otra enumeración para especificar las distintas formas de obtener conexiones para acceder a bases de datos mediante JDBC:

package org.especialistajee.jbib.dao;

public enum TipoFuenteDatos {
  DRIVER, JNDI;
}

Crearemos la siguiente factoría genérica en el paquete org.especialistajee.jbib.dao:

/**
 * $Author$
 * $Date$
 * $Revision$
 */

package org.especialistajee.jbib.dao;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.especialistajee.jbib.BibliotecaException;

/**
 * Singleton que devuelve una factoría de DAOs. El método estático 
 * getInstance() devuelve una factoría de un tipo concreto (MySQL 
 * por defecto). El tipo de factoría puede cambiarse antes de 
 * inicializar el Singleton llamando al método setTipoDaos().
 * 
 * Define los que devuelven los DAO, implementados por las 
 * distintas subclases. Sólo definimos la subclase 
 * FactoriaDaosMySql.
 */
public abstract class FactoriaDaos {
  protected static TipoDaos tipoDaos = TipoDaos.JDBC_MYSQL;
  protected static FactoriaDaos me;
  private static Log logger = 
                   LogFactory.getLog(FactoriaDaos.class.getName());

  /*
   * Se puede llamar a este método antes de crear la FactoriaDaos 
   * para definir el tipo de DAO que se va a crear. De esta forma 
   * permitimos parametrizar el Singleton. Si no se llama a este 
   * método, se crea una factoria de DAOs MySQL.
   */
  public static void setTipoDaos(TipoDaos tipo) 
                                       throws BibliotecaException {
    if (me != null) {
      String msg = "No se puede cambiar el tipo de DAO después de 
                                   haberse creado la FactoriaDaos";
      logger.error(msg);
      throw new BibliotecaException(msg);
    }
    tipoDaos = tipo;
  }

  protected FactoriaDaos() {
  }

  public static FactoriaDaos getInstance() {
    if (me == null)
      switch (tipoDaos) {
      case JDBC_MYSQL:
        me = new FactoriaMySqlDaos();
        break;
      case AMAZON_SIMPLEDB:
      case GOOGLE_BIGTABLE:
        logger.fatal("Tipo de DAOs no implementado");
        throw new DaoException("Tipo de DAOs no implementado");
      }
    return me;
  }
  
  public abstract ILibroDao getLibroDao();
  public abstract IOperacionDao getOperacionDao();
  public abstract IMultaDao getMultaDao();
  public abstract IUsuarioDao getUsuarioDao();
}

Esta factoría utiliza el patrón singleton, pero dicho singleton no es una instancia directa de la factoría, ya que se trata de una clase abstracta, sino de una de sus subclases.

En nuestro caso por el momento sólo crearemos la subclase FactoriaMySqlDaos, que nos permitirá obtener instancias de nuestros diferentes DAOs que accederán mediante JDBC a una base de datos MySQL.

package org.especialistajee.jbib.dao;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.especialistajee.jbib.BibliotecaException;

public class FactoriaMySqlDaos extends FactoriaDaos {
  static private final String URL = 
                               "jdbc:mysql://localhost/biblioteca";
  static private final String USER = "root";
  static private final String PASSWD = "especialista";
  static private final String CLASS = "com.mysql.jdbc.Driver";
  static private TipoFuenteDatos tipoFuenteDatos = 
                                            TipoFuenteDatos.DRIVER;
  static private Log logger =
              LogFactory.getLog(FactoriaMySqlDaos.class.getName());
    
  static protected Connection createConnection() {
    Connection conn = null;
    switch(tipoFuenteDatos) {
    case DRIVER:
      try {
        try {
          Class.forName(CLASS);
        } catch (ClassNotFoundException cnfe) {
          logger.fatal("No se encuentra el Driver de MySQL ",cnfe);
          throw new BibliotecaException(
                       "No se encuentra el Driver de MySQL", cnfe);
        }
        conn = DriverManager.getConnection(URL, USER, PASSWD);
      } catch (SQLException sqle) {
        logger.fatal("No se ha podido crear la conexion", sqle);
      }
      break;
    case JNDI:
      throw new BibliotecaException("No implementado");
    }
    return conn;
  }
  
  public ILibroDao getLibroDao() {
    return new LibroMySqlDao();
  }

  public IUsuarioDao getUsuarioDao() {
    return new UsuarioMySqlDao();
  }
  
  public IOperacionDao getOperacionDao() {
    return new OperacionMySqlDao();
  }
  
  public IMultaDao getMultaDao() {
    return new MultaMySqlDao();
  }
}

Si posteriormente queremos cambiar el tipo de acceso a datos, simplemente deberemos crear una nueva implementación de los DAOs y un nuevo subtipo de factoría. Cambiando el valor del campo tipoDaos de FactoriaDaos podremos alternar entre las diferentes formas de acceso a datos sin tener que modificar el código de nuestra aplicación.

Podemos ver también que hemos aprovechado esta factoría para centralizar la forma de obtener conexiones, ya que desde cada método de los DAOs será necesario obtener una conexión de no hacerlo así tendríamos demasiado código repetido. Además, al tener este código centralizado permitimos cambiar fácilmente la forma en la que dichas conexiones se obtienen, para en el futuro poder utilizar un pool de conexiones del servidor de aplicaciones en lugar de conectar directamente.

Implementación de los DAO

Una vez definidas las interfaces que nos darán acceso a nuestros DAO, deberemos crear una implementación de las mismas. En esta primera sesión crearemos una implementación de los DAO que nos dé acceso a los datos mediante JDBC.

Comenzaremos viendo como ejemplo una implementación de ILibroDao, que recibirá el nombre LibroMySqlDao:

/**
* $Author$
* $Date$
* $Revision$
*/
package org.especialistajee.jbib.dao;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.especialistajee.jbib.model.LibroDomain;

public class LibroMySqlDao implements ILibroDao{

private static Log logger = 
                  LogFactory.getLog(LibroMySqlDao.class.getName());
  
    /* (non-Javadoc)
     * @see org.especialistajee.jbib.dao.ILibroDao#getLibro(
                                                  java.lang.String)
     */
    public LibroDomain getLibro(String isbn) throws DaoException {
    LibroDomain libro = null;

    Connection conn = null;
    PreparedStatement st = null;
    ResultSet rs = null;

    try {
      String query = "select * from libro where isbn = ?";

      if (logger.isDebugEnabled()) {
        logger.debug(query + " [isbn=" + isbn + "]");
      }

      conn = FactoriaMySqlDaos.createConnection();
      st = conn.prepareStatement(query);
      st.setString(1, isbn);
      rs = st.executeQuery();

      if (rs.next()) {
        libro = new LibroDomain();
        libro.setIsbn(rs.getString("isbn"));
        libro.setTitulo(rs.getString("titulo"));
        libro.setAutor(rs.getString("autor"));
        libro.setNumPaginas(rs.getInt("numPaginas"));
        libro.setFechaAlta(rs.getDate("fechaAlta"));
      }
    } catch (SQLException sqle) {
      throw new DaoException("Error en el select de libro", sqle);
    } finally {
      try {
        if (rs != null) {
          rs.close();
          rs = null;
        }
        if (st != null) {
          st.close();
          st = null;
        }
        if (conn != null) {
          conn.close();
          conn = null;
        }
      } catch (SQLException sqlError) {
        throw new RuntimeException("Error cerrando las conexiones",
            sqlError);
      }
    }
    return libro;
  }

  /* (non-Javadoc)
   * @see org.especialistajee.jbib.dao.ILibroDao#addLibro(
                              org.especialistajee.jbib.model.Libro)
   */
  public void addLibro(LibroDomain libro) throws DaoException {
    Connection conn = null;
    PreparedStatement st = null;
    Date fechaAlta = null;

    try {
      String insert = "insert into libro(isbn, titulo, "
          + "autor, numPaginas, fechaAlta) values (?, ?, ?, ?, ?)";

      conn = FactoriaMySqlDaos.createConnection();
      st = conn.prepareStatement(insert);
      st.setString(1, libro.getIsbn());
      st.setString(2, libro.getTitulo());
      st.setString(3, libro.getAutor());
      // insertamos NULL cuando no existe el número de páginas
      if (libro.getNumPaginas() == null) 
        st.setNull(4, java.sql.Types.INTEGER);
      else
        st.setInt(4, libro.getNumPaginas());
      fechaAlta = libro.getFechaAlta();
      if (fechaAlta == null)
        st.setDate(5, null);
      else
        st.setDate(5, new java.sql.Date(fechaAlta.getTime()));
      st.executeUpdate();
    } catch (SQLException sqle) {
      throw new DaoException("Error añadiendo un libro", sqle);
    } finally {
      try {
        if (st != null) {
          st.close();
          st = null;
        }
        if (conn != null) {
          conn.close();
          conn = null;
        }
      } catch (SQLException sqlError) {
        throw new RuntimeException("Error cerrando las conexiones",
            sqlError);
      }
    }
  }

  /* (non-Javadoc)
   * @see org.especialistajee.jbib.dao.ILibroDao#delLibro(
                                                  java.lang.String)
   */
  public int delLibro(String isbn) throws DaoException {
    Connection conn = null;
    PreparedStatement st = null;
    int numDel = 0;

    try {
      String delete = "delete from libro where isbn=?";

      conn = FactoriaMySqlDaos.createConnection();
      st = conn.prepareStatement(delete);
      st.setString(1, isbn);
      numDel = st.executeUpdate();
    } catch (SQLException sqle) {
      throw new DaoException("Error en el delete de libro", sqle);
    } finally {
      try {
        if (st != null) {
          st.close();
          st = null;
        }
        if (conn != null) {
          conn.close();
          conn = null;
        }
      } catch (SQLException sqlError) {
        throw new RuntimeException("Error cerrando las conexiones",
            sqlError);
      }
    }
    return numDel;
  }

  /* (non-Javadoc)
   * @see org.especialistajee.jbib.dao.ILibroDao#getLibros()
   */
  public List<LibroDomain> getLibros() throws DaoException {
    Connection conn = null;
    Statement st = null;
    ResultSet rs = null;
    List<LibroDomain> al = null;

    try {
      conn = FactoriaMySqlDaos.createConnection();
      st = conn.createStatement();
      rs = st.executeQuery("select * from libro");
      al = new ArrayList<LibroDomain>();
      LibroDomain libro = null;
      while (rs.next()) {
        libro = new LibroDomain();
        libro.setIsbn(rs.getString("isbn"));
        libro.setTitulo(rs.getString("titulo"));
        libro.setAutor(rs.getString("autor"));
        libro.setNumPaginas(rs.getInt("numPaginas"));
        libro.setFechaAlta(rs.getDate("fechaAlta"));
        
        al.add(libro);
      }
    } catch (SQLException sqle) {
      throw new DaoException("Error en el select de libro", sqle);
    } finally {
      try {
        if (rs != null) {
          rs.close();
          rs = null;
        }
        if (st != null) {
          st.close();
          st = null;
        }
        if (conn != null) {
          conn.close();
          conn = null;
        }
      } catch (SQLException sqlError) {
        throw new RuntimeException("Error cerrando las conexiones",
            sqlError);
      }
    }
    return al;
  }

  /* (non-Javadoc)
   * @see org.especialistajee.jbib.dao.ILibroDao#updateLibro(
                              org.especialistajee.jbib.model.Libro)
   */
  public int updateLibro(LibroDomain libro) throws DaoException {
    Connection conn = null;
    PreparedStatement st = null;
    int numDel = 0;

    try {
      String update = "update libro set isbn=?, titulo=?, "
          + "autor=?, numPaginas=?, fechaAlta=? where isbn=?";

      conn = FactoriaMySqlDaos.createConnection();
      st = conn.prepareStatement(update);
      st.setString(1, libro.getIsbn());
      st.setString(2, libro.getTitulo());
      st.setString(3, libro.getAutor());
      // insertamos NULL cuando no existe el número de páginas
      if (libro.getNumPaginas() == null) 
        st.setNull(4, java.sql.Types.INTEGER);
      else
        st.setInt(4, libro.getNumPaginas());
      st.setDate(5, 
                new java.sql.Date(libro.getFechaAlta().getTime()));
      st.setString(6, libro.getIsbn());
      numDel = st.executeUpdate();
    } catch (SQLException sqle) {
      throw new DaoException("Error en el update de libro", sqle);
    } finally {
      try {
        if (st != null) {
          st.close();
          st = null;
        }
        if (conn != null) {
          conn.close();
          conn = null;
        }
      } catch (SQLException sqlError) {
        throw new RuntimeException("Error cerrando las conexiones",
            sqlError);
      }
    }
    return numDel;
  }
}

De esta clase destacamos el uso de:

  • El uso de FactoriaMySqlDaos para obtener las conexiones de forma centralizada.
  • Nested exceptions cada vez que ocurre un error en el acceso a los datos. Capturamos la excepción SQLException propia de JDBC, y devolvemos la nuestra propia (DaoException).
  • PreparedStatements para así evitar el SQL injection.
  • Log4J para escribir mensajes de error o depuración.
  • Bloques finally para asegurarnos de que las conexiones siempre se cierran

También podemos observar que hay ciertos aspectos mejorables en el código. Por ejemplo, cuando recuperamos los datos de un libro, tanto en getLibro() como en getLibros() utilizamos el mismo bloque de código:

libro = new LibroDomain();
libro.setIsbn(rs.getString("isbn"));
libro.setTitulo(rs.getString("titulo"));
libro.setAutor(rs.getString("autor"));
libro.setNumPaginas(rs.getInt("numPaginas"));
libro.setFechaAlta(rs.getDate("fechaAlta"));

Para evitar tener código repetido, podríamos refactorizar el código para extraer este bloque a un método independiente, de forma que desde ambas operaciones podamos reutilizar dicho método. Podemos utilizar el ménu Refactor de Eclipse para hacer esto. Seleccionaremos el bloque de operaciones que queremos extraer, pulsamos sobre ellas con el botón derecho, y en el menú Refactor seleccionamos la opción Extract Method .... Extraremos estas operaciones a un método protegido y estático recuperaLibro, que a partir de un objeto ResultSet leerá los datos del libro y nos los devolverá dentro de un LibroDomain:

protected static LibroDomain recuperaLibro(ResultSet rs) 
                                           throws SQLException {
  LibroDomain libro = new LibroDomain();
  libro.setIsbn(rs.getString("isbn"));
  libro.setTitulo(rs.getString("titulo"));
  libro.setAutor(rs.getString("autor"));
  libro.setNumPaginas(rs.getInt("numPaginas"));
  libro.setFechaAlta(rs.getDate("fechaAlta"));
  return libro;
}

Veremos que Eclipse ha modificado automáticamente todos los lugares del código en los que se realizaban las operaciones seleccionadas, y las ha sustituido por la llamada al nuevo método. Así, en getLibro tendremos:

if (rs.next()) {
  libro = this.recuperaLibro(rs);
}

Y de la misma forma, en getLibros tendremos:

while (rs.next()) {
  libro = this.recuperaLibro(rs);				
  al.add(libro);
}

Ahora deberemos crear el DAO OperacionMySqlDao, que implemente todas las operaciones de la interfaz IOperacionDao. Podemos utilizar LibroMySqlDao como ejemplo a seguir.

Del DAO UsuarioMySqlDao sólo será necesario implementar la operación getUsuario. El resto de operaciones de IUsuarioDao y de IMultaDao no es necesario implementarlas por el momento. Bastará con crear stubs vacíos de dichos métodos (esto puede hacerlo Eclipse de forma automática).

Nota
Para la implementació de OperacionMySqlDao, en muchas ocasiones necesitaremos especificar en la sentencia SQL el tipo o el estado de una operación. Para ello podemos servirnos de los elementos de las enumeraciones TipoOperacion y EstadoOperacion que tenemos definidas, evitando de esa forma introducir las cadenas con estos nombres directamente en el código (utilizar los métodos toString() y valueOf de las enumeraciones, para convertir entre cadenas de texto y elementos de la enumeración). Para las fechas utilizaremos los objetos java.sql.Date y java.sql.Time para convertir entre el tipo fecha de Java y la representación en SQL.

Casos de prueba

Una vez hayamos implementado los DAOs, deberemos probarlos para comprobar que funcionan correctamente. Para ello podríamos definir una serie de casos de prueba de JUnit como los siguientes:

/**
* $Author$
* $Date$
* $Revision$
*/

package org.especialistajee.jbib.dao;

import static org.junit.Assert.*;

import java.util.List;

import org.especialistajee.jbib.model.LibroDomain;
import org.junit.Test;

public class LibroDaoTest {
	
  @Test
  public void testGetLibro() throws DaoException {
    ILibroDao libroDao = FactoriaDaos.getInstance().getLibroDao();

    LibroDomain libro = libroDao.getLibro("0321180860");
    assertEquals(new Integer(465), libro.getNumPaginas());
    assertEquals("Understanding SOA with Web Services", 
        libro.getTitulo());
    assertEquals("Eric Newcomer and Greg Lomow", libro.getAutor());
  }

  @Test
  public void testGetLibroNulo() throws DaoException {
    ILibroDao libroDao = FactoriaDaos.getInstance().getLibroDao();

    LibroDomain libro = libroDao.getLibro("1321180860");
    assertNull(libro);
  }

  @Test
  public void testGetLibros() throws DaoException {
    ILibroDao libroDao = FactoriaDaos.getInstance().getLibroDao();

    List<LibroDomain> libros = libroDao.getLibros();
    assertEquals(12, libros.size());
  }

}

Sin embargo, aquí debemos tener en cuenta que todas estas pruebas dependen del estado actual de la base de datos (es una entrada más para nuestras operaciones), lo cual dificulta la realización de estas pruebas de unidad. En este caso, las pruebas se han implementado basándonos en el estado de la base de datos definido en el script datos.sql. Por este motivo, antes de ejecutar las pruebas deberemos asegurarnos de que la base se encuentra en este estado ejecutando dicho script.

Pero aquí no acaban todos los posibles problemas, ya que si estamos probando por ejemplo a hacer una inserción, el estado en el que quedará la base de datos tras hacer esto dependerá de si la inserción ha funcionado bien o mal, lo cual afectaría a las siguientes pruebas. Para evitar que ocurra esto, deberíamos recuperar el estado inicial de la base de datos tras realizar cada prueba. Esto podemos hacerlo por ejemplo con librerías como DBUnit (primeros pasos con DBUnit, artículo sobre DBUnit).

Para simplificar, vamos a limitarnos a utilizar JUnit para operaciones que no modifican la base de datos, y que por lo tanto no producirán problemas de este tipo.

Al igual que hemos realizado pruebas con los métodos de consulta de ILibroDao, vamos ahora a implementar casos de prueba para IUsuarioDao, concretamente para su método getUsuario(login) (se puede probar a obtener un usuario válido y uno no válido).

Recuerda
Los casos de prueba de JUnit deben situarse en el mismo paquete que la clase probada, pero en diferente directorio de fuentes (en src/test en lugar de src/main).

Prueba de métodos de negocio

Una vez probados algunos de los métodos individuales de nuestros DAO, vamos a realizar una aplicación de prueba que implemente algún caso de uso de nuestra aplicación, apoyándonos en los diferentes DAO que tenemos. Esta aplicación se ejecutará en la consola, y nos mostrará un menú con tres opciones:

  1. Obtener listado de libros disponibles
  2. Obtener listado de libros reservados
  3. Realizar reserva

La primera de ellas se basará en la operación getLibrosDisponibles() de IOperacionDao para obtener todos los libros de la base de datos que no tengan ninguna operación activa asociada, y por lo tanto estén disponibles para reserva y préstamo.

La segunda utilizará el método getOperaciones(RESERVA, ACTIVA) para obtener un listado de todos los libros reservados actualmente, y mostrar sus datos junto al login del usuario que hizo la reserva.

La tercera necesitará combinar diferentes llamadas a los DAO y a las reglas de negocio definidas, para implementar así la funcionalidad de este método de negocio.

Podemos descargar la implementación de esta aplicación PruebaDao y añadirla a nuestra aplicación. De especial interés es observar su método realizaReserva(isbn, login), en el que se implementa la lógica de negocio de esta operación, utilizando para ello las reglas de negocio y los diferentes DAOs definidos.

Una vez añadida esta clase, podremos ejecutarla desde Eclipse o directamente desde línea de comandos con Maven:

mvn exec:java 
        -Dexec.mainClass=org.especialistajee.jbib.dao.PruebaDao
        -Dexec.classpathScope=test
Nota
Dado que la clase PruebaDao no pertenecerá a la aplicación final, y simplemente se utiliza como driver para realizar una prueba de la aplicación, la ubicaremos en el directorio de fuentes src/test/java. No obstante, más adelante podríamos coger de aquí el método realizaReserva para implementar la lógica de negocio de la aplicación.

A entregar

Proyecto a entregar

En esta sesión deberemos entregar el proyecto proy-int-comun, al que habremos añadido los DAO para acceder a la base de datos mediante JDBC. Sobre el proyecto de la anterior sesión, deberemos añadir:

  • Nuevo proyecto Maven jbib-persist-dao.
  • Ficheros de configuración para log4j (log4j.properties) y commons logging (commons-logging.properties) en src/main/resources.
  • Scripts para crear e inicializar la base de datos: biblioteca.sql en src/main/sql, y datos.sql en src/test/sql.
  • Fichero POM configurado con dependencias del driver JDBC de MySQL, de junit, de log4j, de commons logging, y del proyecto común, y para que ejecute los scripts para crear la base de datos antes de realizar los tests mediante el plugin SQL.
  • Excepción DaoException para tratar los errores ocurridos en los DAO de la aplicación.
  • Interfaces de los diferentes DAOs: ILibroDao, IUsuarioDao, IMultaDao, e IOperacionDAO.
  • Factoría genérica FactoriaDaos y subtipo para instanciar los DAOs de MySql FactoriaMySqlDaos.
  • Implementaciones MySQL de los DAOs: LibroMySqlDao, UsuarioMySqlDao, MultaMySqlDao, y OperacionMySqlDao. Por el momento es necesario implementar todas las operaciones de LibroMySqlDao y OperacionMySqlDao, y la operación getUsuario de UsuarioMySqlDao. Para el resto basta con crear stubs vacíos.
  • Casos de prueba JUnit LibroDaoTest y UsuarioDaoTest.
  • Driver PruebaDao para probar la funcionalidad de reserva de libros.

El plazo final de entrega será el jueves próximo. Se realizará una sesión on-line de dudas el miércoles de 19:00 a 21:00.