Proyecto Java EE
 

Capa de datos y negocios con JPA

Creación de entidades de dominio con JPA

El objetivo de esta primera parte de la sesión es añadir al proyecto jbib-modelo las clases y anotaciones necesarias para convertir las clases de dominio en entidades persistentes basadas en JPA. Adelantando algunos detalles, en esta sesión deberemos desarrollar los siguientes elementos:

  • Añadir la configuración de JPA al proyecto jbib-modelo:
    • Dependencias de Hibernate en el fichero de configuración de Maven pom.xml
    • Fichero de configuración de la unidad de persistencia JPA META-INF/persistence.xml (del ámbito principal y del test)
  • Añadir anotaciones en las clases de dominio para mapearlas en tablas
  • Validar el esquema SQL resultante de la base de datos
  • Desarrollar las clases DAO que proporcionan una capa de abstracción para la recuperación, modificación y borrado de las entidades
  • Comprobar y ampliar tests que comprueben el funcionamiento de las entidades y de los DAO utilizando DbUnit

Vamos a ello paso a paso.

Configuración de JPA

En primer lugar debes añadir en el fichero de Maven pom.xml del proyecto jbib-modelo las dependencias necesarias para trabajar con:

  • JPA/Hibernate
  • bases de datos MySQL y Apache Derby. Apache Derby es una base de datos que funciona en memoria y que vamos a utilizar para hacer más eficientes los tests.
  • Plugin hibernate3-maven para poder generar el esquema de la base de datos por línea de comandos

Las dependencias son las siguientes:

<dependencies>

    <!-- MySql Connector -->

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.10</version>
        <scope>runtime</scope>
    </dependency>

     <!-- Hibernate -->

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>3.6.10.Final</version>
    </dependency>

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.6.1</version>
        <scope>runtime</scope>
    </dependency>

</dependencies>

<build>
    <plugins>
	     
         <!-- Plugin hibernate3-maven -->

         <plugin>
             <groupId>org.codehaus.mojo</groupId>
             <artifactId>hibernate3-maven-plugin</artifactId>
             <version>2.2</version>
             <configuration>
                 <components>
                     <component>
                         <name>hbm2ddl</name>
                         <implementation>jpaconfiguration</implementation>
                     </component>
                 </components>
                 <componentProperties>
                     <persistenceunit>biblioteca</persistenceunit>
                     <outputfilename>schema.ddl</outputfilename>
                     <drop>false</drop>
                     <create>true</create>
                     <export>false</export>
                     <format>true</format>
                 </componentProperties>
             </configuration>
         </plugin>
     </plugins>
</build>

Añadimos los ficheros de configuración de JPA en el ámbito de test de Maven. El ámbito de test lo usaremos para crear una base de datos vacía, poblarla con datos de prueba y comprobar que las clases funcionan correctamente.

Copia los ficheros commons-logging.properties y log4j.properties al ámbito de test (src/test/resources/) y añade las siguientes líneas al segundo, para activar los logs de Hibernate a nivel de INFO.

# Hibernate logging options (INFO only shows startup messages)
log4j.logger.org.hibernate=INFO

# Log JDBC bind parameter runtime arguments
#log4j.logger.org.hibernate.type=ALL

Continua creando el fichero de configuración de JPA en el directorio de recursos de tests src/test/resources/META-INF/persistence.xml con el siguiente contenido:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="1.0"
   xmlns="http://java.sun.com/xml/ns/persistence" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd">

   <persistence-unit name="biblioteca" transaction-type="RESOURCE_LOCAL">
	
   <properties>
      <property name="hibernate.dialect" 
	            value="org.hibernate.dialect.MySQLInnoDBDialect" />
      <property name="hibernate.connection.driver_class" 
	            value="com.mysql.jdbc.Driver" />
      <property name="hibernate.connection.username" value="root" />
      <property name="hibernate.connection.password" value="expertojava" />
      <property name="hibernate.connection.url"
	            value="jdbc:mysql://localhost:3306/biblioteca" />
      <property name="hibernate.hbm2ddl.auto" value="create" />
      <property name="hibernate.show_sql" value="true" />
   </properties>
   </persistence-unit>
</persistence>

Es importante hacer notar que el valor de hibernate.hbm2ddl.auto está puesto a create. Esto significa que el esquema de base de datos se creará de nuevo cada vez que lancemos los tests.

Nota
En esta primera parte de la sesión vamos a crear el esquema inicial de base de datos a partir del modelo de clases. Después refinaremos el modelo de clases, añadiendo anotaciones @Table, @Column y @JoinColumn para adaptarlo al esquema SQL único que proporcionaremos más adelante.

Test de la unidad de persistencia

Creamos el test que carga la unidad de persistencia vacía, en la que todavía no hemos añadido ninguna entidad. Lo hacemos en el directorio src/test/java y en el paquete es.ua.jtech.jbib.persistence:

package es.ua.jtech.jbib.persistence;

public class PersistenceUnitTest {

   private static Log logger = LogFactory
          .getLog(PersistenceUnitTest.class);
   private static EntityManagerFactory emf;

   @BeforeClass
   public static void initDatabaseTest() {
      try {
         logger.info("Building JPA EntityManagerFactory for unit tests");
         emf = Persistence.createEntityManagerFactory("biblioteca");
      } catch (Exception ex) {
         ex.printStackTrace();
         fail("Exception during JPA EntityManager instanciation.");
      }
   }

   @AfterClass
   public static void closeEntityManagerFactory() {
      logger.info("Closing JPA EntityManagerFactory");
      if (emf != null)
         emf.close();
   }
}

La anotación @BeforeClass hace que el método initDatabase() se ejecute sólo una vez, antes de ejecutar todos los tests. En el método se obtiene la factoría de entity managers y se guarda en la variable estática emf a la que se accederá desde todos los tests para obtener entity managers.

La anotación AfterClass hace que el método se ejecute al terminar todos los tests y cierra la factoría de entity managers.

Lanza el test desde Eclipse y comprueba que todo funciona correctamente.

Abre un terminal y lanza todos los tests del proyecto usando Maven:

$ cd proyint-expertojava/jbib
$ mvn test

Anotaciones de las clases de dominio

Vamos ahora a crear las entidades anotando todas las clases de dominio y creando un esquema de base de datos inicial.

Empieza añadiendo todas las entidades en el fichero src/test/resources/META-INF/persistence.xml

<persistence-unit ...>
	<class>es.ua.jtech.jbib.model.EjemplarDomain</class>
    <class>es.ua.jtech.jbib.model.LibroDomain</class>
    <class>es.ua.jtech.jbib.model.ReservaDomain</class>
    <class>es.ua.jtech.jbib.model.UsuarioDomain</class>
    <class>es.ua.jtech.jbib.model.AlumnoDomain</class>
    <class>es.ua.jtech.jbib.model.ProfesorDomain</class>
    <class>es.ua.jtech.jbib.model.BibliotecarioDomain</class>
    <class>es.ua.jtech.jbib.model.PrestamoHistoricoDomain</class>
    <class>es.ua.jtech.jbib.model.MultaDomain</class>

    <properties>
	...

Y añade también un test básico que prueba a cargar el entity manager en la clase PersistenceUnitTest:

   @Test
   public void createEntityManagerTest() {
      EntityManager em = emf.createEntityManager();
      assertNotNull(em);
      em.close();
   }

Si lanzas ahora los tests con mvn test verás que salta un error debido a que las entidades no están definidas. Debes añadir ahora las anotaciones necesarias en las clases de dominio para mapear las entidades con las tablas de la base de datos. Los tipos de los campos deben corresponder con los definidos en las clases.

Para cada entidad añade un nuevo atributo llamado id de tipo Long que defina su clave primaria en la base de datos. Mapéalo con la anotación @Id @GeneratedValue(strategy=GenerationType.AUTO).

Hay que añadir las anotaciones de JPA a las siguientes clases en el paquete es.ua.jtech.jbib.model:

  • EjemplarDomain
  • LibroDomain
  • MultaDomain
  • UsuarioDomain (abstracta) y su relación de herencia con AlumnoDomain y ProfesorDomain
  • BibliotecarioDomain
  • PrestamoHistorioDomain

Como referencia, mostramos a continuación el esquema SQL completo de la base de datos resultante:

create database biblioteca;
use biblioteca; 

create table bibliotecario (
   id bigint not null auto_increment,
   email varchar(255),
   login varchar(255) unique,
   nif varchar(255),
   password varchar(255),
   primary key (id)
) type=InnoDB;

create table ejemplar (
   id bigint not null auto_increment,
   fechaAdquisicion date,
   fechaDevolucion date,
   fechaPrestamo date,
   idEjemplar varchar(255),
   localizacion enum('SALA','CASA','DEPARTAMENTO') not null,
   observaciones varchar(255),
   libro_id bigint not null,
   usuario_id bigint,
   primary key (id)
) type=InnoDB;

create table historico (
   id bigint not null auto_increment,
   fechaDevolucion date,
   fechaDevolucionReal date,
   fechaPrestamo date,
   ejemplar_id bigint not null,
   usuario_id bigint not null,
   primary key (id)
) type=InnoDB;

create table libro (
   id bigint not null auto_increment,
   autor varchar(255),
   fechaAlta date,
   isbn varchar(255) unique,
   numDisponibles integer,
   numPaginas integer,
   titulo varchar(255),
   primary key (id)
) type=InnoDB;

create table multa (
   id bigint not null auto_increment,
   estado enum('ACTIVA','HISTORICA') not null,
   fechaFin date,
   fechaInicio date,
   usuario_id bigint not null,
   primary key (id)
) type=InnoDB;

create table reserva (
   id bigint not null auto_increment,
   estado enum('ACTIVA','HISTORICA') not null,
   fecha date,
   fechaFin date,
   tipoFinal enum('CANCELADO','FINALIZADO'),
   libro_id bigint not null,
   usuario_id bigint not null,
   primary key (id)
) type=InnoDB;

create table usuario (
   tipo varchar(31) not null,
   id bigint not null auto_increment,
   apellido1 varchar(255),
   apellido2 varchar(255),
   calle varchar(255),
   ciudad varchar(255),
   codigoPostal varchar(255),
   numero integer,
   piso varchar(255),
   email varchar(255) unique,
   estado enum('ACTIVO', 'MOROSO', 'MULTADO') not null,
   login varchar(255) unique,
   nombre varchar(255),
   password varchar(255),
   tutor varchar(255),
   departamento varchar(255),
   primary key (id)
) type=InnoDB;

alter table ejemplar 
   add index FK95B940BAA013FCAC (usuario_id), 
   add constraint FK95B940BAA013FCAC 
   foreign key (usuario_id) 
   references usuario (id);

alter table ejemplar 
   add index FK95B940BA930E18AC (libro_id), 
   add constraint FK95B940BA930E18AC 
   foreign key (libro_id) 
   references libro (id);

alter table historico 
   add index FK66D8DFF0A013FCAC (usuario_id), 
   add constraint FK66D8DFF0A013FCAC 
   foreign key (usuario_id) 
   references usuario (id);

alter table historico 
   add index FK66D8DFF0F97DB50 (ejemplar_id), 
   add constraint FK66D8DFF0F97DB50 
   foreign key (ejemplar_id) 
   references ejemplar (id);

alter table multa 
   add index FK636D531A013FCAC (usuario_id), 
   add constraint FK636D531A013FCAC 
   foreign key (usuario_id) 
   references usuario (id);

alter table reserva 
   add index FK41640CB8A013FCAC (usuario_id), 
   add constraint FK41640CB8A013FCAC 
   foreign key (usuario_id) 
   references usuario (id);

alter table reserva 
   add index FK41640CB8930E18AC (libro_id), 
   add constraint FK41640CB8930E18AC 
   foreign key (libro_id) 
   references libro (id);

Listamos a continuación como ejemplo las clases EjemplarDomain, UsuarioDomain y sus clases hijas AlumnoDomain y ProfesorDomain:

Clase EjemplarDomain:

package es.ua.jtech.jbib.model;

// imports 

@Entity
@Table(name = "ejemplar")
public class EjemplarDomain extends DomainObject {

   private static final long serialVersionUID = 1L;

   @Id
   @GeneratedValue(strategy = GenerationType.AUTO)
   private Long id;

   private String idEjemplar;
   @Enumerated(EnumType.STRING)
   @Column(columnDefinition="enum('SALA','CASA','DEPARTAMENTO')",
           nullable=false)
   private Localizacion localizacion;
   @Temporal(TemporalType.DATE)
   private Date fechaAdquisicion;
   private String observaciones;
   @Temporal(TemporalType.DATE)
   private Date fechaPrestamo;
   @Temporal(TemporalType.DATE)
   private Date fechaDevolucion;

   @ManyToOne
   @JoinColumn(nullable = false)
   private LibroDomain libro;
   @OneToOne
   private UsuarioDomain usuario;
   @OneToMany(mappedBy = "ejemplar")
   private Set<PrestamoHistoricoDomain> prestamosHistoricos = 
                      new HashSet<PrestamoHistoricoDomain>();

   private EjemplarDomain() {}

   public EjemplarDomain(LibroDomain libro, String idEjemplar) {
      super();
      this.libro = libro;
      this.idEjemplar = idEjemplar;
      this.localizacion = Localizacion.SALA;
   }

   public Long getId() {
      return id;
   }
   
   // getters y setters

   public EstadoEjemplar getEstado() {
      if (usuario == null)
         return EstadoEjemplar.DISPONIBLE;
      else
         return EstadoEjemplar.PRESTADO;
   }

   @Override
   public int hashCode() {
      final int prime = 31;
      int result = 1;
      result = prime * result
            + ((idEjemplar == null) ? 0 : idEjemplar.hashCode());
      result = prime * result
            + ((libro == null) ? 0 : libro.hashCode());
      return result;
   }

   @Override
   public boolean equals(Object obj) {
      if (this == obj)
         return true;
      if (obj == null)
         return false;
      if (getClass() != obj.getClass())
         return false;
      EjemplarDomain other = (EjemplarDomain) obj;
      if (idEjemplar == null) {
         if (other.idEjemplar != null)
            return false;
      } else if (!idEjemplar.equals(other.idEjemplar))
         return false;
      if (libro == null) {
         if (other.libro != null)
            return false;
      } else if (!libro.equals(other.libro))
         return false;
      return true;
   }
}

Es importante hacer notar lo siguiente:

  • Añadimos la anotación @Entity, la anotación @Table para especificar el nombre de la tabla y anotaciones @Column para el nombre de ciertas columnas. Añadimos también la anotación @Id para definir la clave primaria y las relaciones @OneToMany y @ManyToOne con la especificación de qué atributo se usa para la clave ajena (con mappedBy).
  • Añadimos también un constructor vacío privado en todas las entidades, necesario para Hibernate.
  • Definimos el mapeado del atributo enumerado localización con columnDefinition="enum('SALA','CASA','DEPARTAMENTO')". Esto genera esa definición en la base de datos, para que el valor de la columna pueda ser únicamente SALA, CASA o DEPARTAMENTO. Un inconveniente de esta definición es que no es estándar y sólo se puede utilizar en MySQL y otras pocas bases de datos.

    Debemos añadir esta definición en todos los atributos enumerados.

  • Utilizamos el modificador nullable = false en todos los campos que queremos mapear a columnas NOT NULL. Es recomendable definir en la entidad un constructor que contenga todos los campos definidos como no nulos. De esta forma se obliga a proporcionar esos valores en su creación. En este caso se inicializa a SALA en el constructor.
  • Utilizamos el modificador unique = true en todos los campos que queremos mapear a columnas UNIQUE. Es recomendable hacerlo en aquellos campos por los que después se va a realizar una búsqueda, a parte de la clave primaria. Por ejemplo, el ISBN en un libro o el correo electrónico en un usuario.
  • Es destacable el método getEstado() que devuelve el estado de un ejemplar. El ejemplar no tiene ningún atributo que defina su estado, sino que se devolverá DISPONIBLE o PRESTADO dependiendo de si tiene algún usuario asignado o no.

Clase UsuarioDomain:

package es.ua.jtech.jbib.model;

// imports

@Entity
@Table(name = "usuario")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "tipo", 
                     discriminatorType=DiscriminatorType.STRING)
@DiscriminatorOptions(force = true)
public abstract class UsuarioDomain extends DomainObject {
   private static final long serialVersionUID = 1L;

   @Id @GeneratedValue
   private Long id;

   @Column(unique=true)
   private String login;
   private String password;
   @Column(unique=true)
   private String email;
   private String nombre;
   private String apellido1;
   private String apellido2;
   @Embedded
   private Direccion direccion;

   @Enumerated(EnumType.STRING)
   @Column(columnDefinition="enum('ACTIVO', 'MOROSO', 'MULTADO')",
                             nullable=false)
   private EstadoUsuario estado;
   
   @OneToMany(mappedBy="usuario")
   private Set<EjemplarDomain> prestamos = new HashSet<EjemplarDomain>();
   @OneToMany(mappedBy="usuario")
   private Set<ReservaDomain> reservas = new HashSet<ReservaDomain>();
   @OneToMany(mappedBy="usuario")
   private Set<PrestamoHistoricoDomain> prestamosHistoricos = 
                   new HashSet<PrestamoHistoricoDomain>();
   @OneToMany(mappedBy="usuario")
   private Set<MultaDomain> multas = new HashSet<MultaDomain>();

   // getters, setters, hashCode y equals
}

Además de las anotaciones similares a las de Ejemplar debemos añadir las anotaciones que mapean la relación de herencia con la anotación @Inheritance. Usamos la estrategia de tabla única. La columna discriminante la definimos con la anotación @DiscriminatorColumn. Definimos que su nombre es tipo y que los valores en la columna van ser STRING.

La anotación @DiscriminatorOptions(force = true) es necesaria para que Hibernate pueda construir correctamente objetos de las clases hijas a partir de consultas realizadas sobre la clase padre. En nuestro caso, definiremos una consulta UsuarioDomain.findAll que va devolver todos los objetos AlumnoDomain y ProfesorDomain existentes en la base de datos.

Clase AlumnoDomain:

package es.ua.jtech.jbib.model;

// imports

@Entity
@DiscriminatorValue(value = "ALUMNO")
public class AlumnoDomain extends UsuarioDomain {

   static final long serialVersionUID = 1L;
   String tutor;

   private AlumnoDomain() {}

   public AlumnoDomain(String login, String password) {
      super();
      this.setLogin(login);
      this.setPassword(password);
      this.setEstado(EstadoUsuario.ACTIVO);
   }

   public String getTutor() {
      return tutor;
   }

   public void setTutor(String tutor) {
      this.tutor = tutor;
   }
}

Clase ProfesorDomain:

package es.ua.jtech.jbib.model;

// imports

@Entity
@DiscriminatorValue(value = "PROFESOR")
public class ProfesorDomain extends UsuarioDomain {
   private static final long serialVersionUID = 1L;
   private String departamento;

   private ProfesorDomain {}

   public ProfesorDomain(String login, String password) {
      super();
      this.setLogin(login);
      this.setPassword(password);
      this.setEstado(EstadoUsuario.ACTIVO);
   }

   public String getDepartamento() {
      return departamento;
   }

   public void setDepartamento(String departamento) {
      this.departamento = departamento;
   }
}

Lo único que hay que definir en las clases hijas es la anotación @DiscriminatorValue que define el valor de la columna discriminante que define objetos de cada entidad.

Escribe el resto de entidades, hasta que no haya ningún error al lanzar el test con $mvn test

Validación del esquema resultante

Vamos a usar el esquema SQL anterior para validar el mapeo. Esta validación constituye un test de integración, en el que se comprueba que las entidades desarrolladas van a mapearse bien con la base de datos que tenemos en producción. Para ello hacemos lo siguiente:

1. Creamos la base de datos a partir del esquema SQL anterior. Para ello borramos la base de datos biblioteca con el administrador de MySQL y copiamos el código SQL anterior en un fichero, por ejemplo biblioteca.sql. Para crear la base de datos biblioteca ejecutamos los comandos SQL:

$ mysql -u root -p < bibilioteca.sql

2. Copiamos el fichero persistence.xml a la ruta src/main/resources/META-INF/persistence.xml y cambiamos el valor de hibernate.hbm2ddl.auto a validate.

3. Creamos un programa en el paquete es.ua.jtech.main en el ámbito principal de Maven que cargue esta unidad de persistencia y realice la validación.

En src/main/java:

package es.ua.jtech.jbib.main;

import javax.persistence.EntityManager;
import javax.persistence.EntityManagerFactory;
import javax.persistence.Persistence;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BibliotecaMain {
   private static Log logger = LogFactory.getLog(BibliotecaMain.class);

   public static void main(String[] args) {
      logger.info("Building JPA EntityManagerFactory for DB validation");
      EntityManagerFactory emf = Persistence.createEntityManagerFactory("biblioteca");
      EntityManager em = emf.createEntityManager();
      em.close();
      emf.close();
      logger.info("JPA EntityManagerFactory closed");
   }
}

4. Ejecutamos el programa desde Eclipse o desde Maven con:

$ mvn exec:java -Dexec.mainClass="es.ua.jtech.jbib.main.BibliotecaMain"

Comprobamos que la validación es correcta y no hay ningún error.

Clases DAO

Una vez construidas las entidades debemos crear las clases DAO que abstraerán las operaciones comunes CRUD y las consultas. Las debemos crear en el paquete es.ua.jtech.jbib.persistence. Nos basamos en el patrón visto en la sesión correspondiente de JPA. Creamos las siguientes clases:

  • Dao
  • BibliotecarioDao
  • EjemplarDao
  • LibroDao
  • MultaDao
  • PrestamoHistoricoDao
  • ReservaDao
  • UsuarioDao

Definiremos las funciones de creación, actualización y borrado, y también una consulta para cada una de las entidades que devuelve todas las instancias (hay que añadirla en cada una de las clases de dominio). Más adelante incluiremos más consultas.

Listamos a continuación la clase genérica Dao, ligeramente modificada con respecto a la vista en la sesión de JPA.

package es.ua.jtech.jbib.persistence;

import javax.persistence.EntityExistsException;
import javax.persistence.EntityManager;
import javax.persistence.TransactionRequiredException;

abstract class Dao<T, K> {
   EntityManager em;

   /**
    * Constructor del DAO. Se pasa como parámetro un entity manager que va a
    * utilizar en todas sus operaciones. El entity manager debe tener abierta
    * una transacción. Quien llama al DAO es el responsable de abrir y cerrar la
    * transacción y el entity manager.
    * 
    * @param em entity manager
    * @throws TransactionRequiredException cuando se llama al constructor del
    *            DAO sin que se haya abierto una transacción en el entity
    *            manager
    */
   public Dao(EntityManager em) {
      if (!em.getTransaction().isActive())
         throw new TransactionRequiredException(
               "Es necesaria una transacción activa para construir el DAO");
      this.em = em;
   }

   /**
    * @return el entity manager asociado al DAO
    */
   public EntityManager getEntityManager() {
      return this.em;
   }

   /**
    * Devuelve la entidad correspondiente a una clave primaria.
    * 
    * @param id clave primaria
    * @return la entidad encontrada o null si la entidad no existe
    */
   public abstract T find(K id);

   /**
    * Hace persistente una entidad, obteniéndose su clave primaria en el caso de
    * que ésta sea generada. La entidad no debe estar gestionada por el entity
    * manager.
    * 
    * @param t entidad que se quiere hacer persistente
    * @return entidad persistente gestionada por el entity manager, con su clave
    *         primaria generada
    * @throws DaoException si la entidad que se pasa ya está creada
    * @throws IllegalArgumentException si el objeto que se pasa no es una
    *            entidad
    */
   public T create(T t) {
      try {
         em.persist(t);
         em.flush();
         em.refresh(t);
         return t;
      } catch (EntityExistsException ex) {
         throw new DaoException("La entidad ya existe", ex);
      }
   }

   /**
    * Actualiza los valores de una entidad en el gestor de persistencia. La
    * entidad debe tener la clave primaria y puede no estar gestionada por el
    * entity manager.
    * 
    * @param t la entidad persistente que se quiere actualizar
    * @return la entidad persistente gestionada
    * @throws IllegalArgumentException Si el objeto que se pasa no es una
    *            entidad.
    */
   public T update(T t) {
      return (T) em.merge(t);
   }

   /**
    * Elimina la entidad persistente que se pasa como parámetro. Debe tener la
    * clave primaria y puede no estar gestionada por el entity manager.
    * 
    * @param t la entidad persistente que se quiere eliminar
    * @throws IllegalArgumentException Si el objeto que se pasa no es una
    *            entidad
    */
   public void delete(T t) {
      t = em.merge(t);
      em.remove(t);
   }
}

Cambios con respecto a la sesión de JPA:

  • Se comprueba que existe una transacción abierta en el entity manager al crear el DAO.
  • Se documentan las excepciones de tipo Runtime que arrojan los métodos.
  • Se crea una excepción de tipo DaoException que envuelve la excepción de JPA EntityExistsException.

Listamos también las clases EjemplarDao y DaoException:

package es.ua.jtech.jbib.persistence;

import java.util.List;
import javax.persistence.EntityManager;
import es.ua.jtech.jbib.model.BibliotecarioDomain;

public class BibliotecarioDao extends Dao<BibliotecarioDomain, Long> {

   public BibliotecarioDao(EntityManager em) {
      super(em);
   }

   @Override
   public BibliotecarioDomain find(Long id) {
      EntityManager em = this.getEntityManager();
      return em.find(BibliotecarioDomain.class, id);
   }
   
  /**
    * @return una lista con todas las entidades Bibliotecario existentes
    */
   @SuppressWarnings("unchecked")
   public List<BibliotecarioDomain> getAllBibliotecarios() {
      EntityManager em = this.getEntityManager();
      // getResultList puede devolver una QueryTimeoutException,
      // pero no la vamos a tratar, porque se produce en una situación
      // excepcional no controlable
      return (List<BibliotecarioDomain>) em
            .createNamedQuery("Bibliotecario.findAll").getResultList();
   }
}

La clase DaoException:

package es.ua.jtech.jbib.persistence;

public class DaoException extends RuntimeException {
   private static final long serialVersionUID = 1L;

   public DaoException(String mensaje) {
      super(mensaje);
   }

   public DaoException(String mensaje, Throwable causa) {
      super(mensaje, causa);
   }
}

Tests de los DAO con DbUnit

Vamos por último a escribir los tests que comprueban los DAOs desarrollando utilizando DbUnit para poblar la base de datos en el ámbito de test de Maven.

Añadimos la dependencia en el POM del proyecto jbib-modelo:

<!-- DbUnit -->

<dependency>
    <groupId>org.dbunit</groupId>
    <artifactId>dbunit</artifactId>
    <version>2.4.9</version>
    <scope>test</scope>
</dependency>

Creamos una primera versión de la clase de test es.ua.jtech.jbib.persistence.DaoTest en src/test/java/ en la que comprobamos cómo cargar la conexión de DbUnit:

package es.ua.jtech.jbib.persistence;

// imports

public class DaoTest {
   private static EntityManagerFactory emf;
   private static IDatabaseConnection connection;
   private static IDataSet dataset;

   @BeforeClass
   public static void initDatabaseTest() {
      try {
         emf = Persistence.createEntityManagerFactory("biblioteca");
         Class.forName("com.mysql.jdbc.Driver");
         Connection jdbcConnection = (Connection) DriverManager
               .getConnection(
                     "jdbc:mysql://localhost:3306/biblioteca",
                     "root", "expertojava");
         connection = new DatabaseConnection(jdbcConnection);

         FlatXmlDataSetBuilder flatXmlDataSetBuilder = new FlatXmlDataSetBuilder();
         flatXmlDataSetBuilder.setColumnSensing(true);
         dataset = flatXmlDataSetBuilder.build(Thread.currentThread()
               .getContextClassLoader()
               .getResourceAsStream("test-dao-dataset.xml"));
      } catch (Exception ex) {
         ex.printStackTrace();
         fail("Exception during JPA EntityManager instanciation.");
      }
   }

   @AfterClass
   public static void closeEntityManagerFactory() throws Exception {
      DatabaseOperation.DELETE_ALL.execute(connection, dataset);      
      if (emf != null)
         emf.close();
   }

   @Before
   public void cleanDB() throws Exception {
      DatabaseOperation.CLEAN_INSERT.execute(connection, dataset);
   }

   @Test
   public void bibliotecarioDaoCreateTest() {
      EntityManager em = emf.createEntityManager();
      em.getTransaction().begin();

      // Obtenemos el BibliotecarioDao
      BibliotecarioDao bibliotecarioDao = new BibliotecarioDao(em);

      // Creamos un nuevo bibliotecario
      BibliotecarioDomain bibliotecario = new BibliotecarioDomain(
            "francisco.perez", "123456");
      bibliotecarioDao.create(bibliotecario);
      assertNotNull(bibliotecario.getId());

      // Guardamos su id y cerramos la transacción y el em
      Long id = bibliotecario.getId();
      em.getTransaction().commit();
      em.close();

      // Abrimos otro em para comprobar que el bibliotecario
      // está en la BD
      em = emf.createEntityManager();
      bibliotecarioDao = new BibliotecarioDao(em);
      BibliotecarioDomain bib2 = bibliotecarioDao.find(id);
      assertNotNull(bib2);
      assertTrue(bib2.getLogin().equals(bibliotecario.getLogin())
            && bib2.getPassword().equals(bibliotecario.getPassword()));
      em.close();
   }
}

Algunos comentarios:

  • La anotación @BeforeClass hace que el método initDabaseTest() se ejecute una sola vez antes de todos los tests.
  • Este método initDatabaseTest obtiene la factoría de entity managers, el driver MySQL, la conexión a la base de datos y el dataset test-dao-dataset.xml
  • El método closeEntityManagerFactory() se ejecuta al final de todos los tests, cierra la factoría de entity managers y borra todos los registros en las tablas del dataset
  • El método cleanDB() tiene la anotación @Before que hace que se ejecute antes de cada uno de los tests. En él se inicializa la base de datos con los registros definidos en el dataset. De esta forma nos aseguramos que cada test comienza con el mismo estado de la base de datos.
  • El test bibliotecarioDaoCreateTest() no obtiene ningún dato del dataset. Crea un bibliotecario nuevo en la base de datos usando el BibliotecarioDao y después comprueba que se ha insertado correctamente buscándolo por su clave primaria.

Creamos el fichero src/test/resources/test-daos-dataset.xml vacío:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
</dataset>

Lanzamos el test DaoTest y compruebamos que no hay ningún error.

Terminamos añaiendo algunos datos al dataset y añadiendo algunos tests al DaoTest:

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
    <bibliotecario id="1" login="javier.huertas" password="11111"/>
    <bibliotecario id="2" login="francisco.martinez" password="22222"/>
    <usuario tipo="ALUMNO"/>
    <usuario tipo="ALUMNO"/>
    <usuario tipo="PROFESOR"/>
</dataset>

Tests:

// Test que busca el bibliotecario con id=1
@Test
public void bibliotecarioDaoFindTest() {
   EntityManager em = emf.createEntityManager();
   em.getTransaction().begin();
	      BibliotecarioDao bibliotecarioDao = new BibliotecarioDao(em);
   BibliotecarioDomain bib2 = bibliotecarioDao.find(1L);
   assertNotNull(bib2);
   assertTrue(bib2.getLogin().equals("javier.huertas")
         && bib2.getPassword().equals("11111"));
   em.getTransaction().commit();
   em.close();
}
	
	
// Test que comprueba que getAllBibliotecarios() devuelve 2 bibliotecarios
@Test
public void getAllBibliotecariosTest() {
   EntityManager em = emf.createEntityManager();
   em.getTransaction().begin();
    
   BibliotecarioDao bibliotecarioDao = new BibliotecarioDao(em);
   List<BibliotecarioDomain> lista = bibliotecarioDao.getAllBibliotecarios();
   assertTrue(lista.size() == 2);
    
   em.getTransaction().commit();
   em.close();
}


// Test que comprueba que getAllUsuarios devuelve 3 usuarios
@Test
public void getAllUsuariosTest() {
   EntityManager em = emf.createEntityManager();
   em.getTransaction().begin();
    
   UsuarioDao usuarioDao = new UsuarioDao(em);
   List<UsuarioDomain> listaUsuarios = usuarioDao.getAllUsuarios();
   assertTrue(listaUsuarios.size() == 3);
    
   em.getTransaction().commit();
   em.close();
}

Añade registros en el dataset y tests que comprueben:

  • Todos los métodos getAll del resto de los DAOs

Singleton PersistenceManager

Desde la capa de negocio de la aplicación vamos a necesitar obtener entity managers. Para eso añadimos el singleton PersistenceManager que proporciona esta utilidad.

En el método de creación del singleton obtenemos la factoría de entity managers y la guardamos en una variable estática. Definimos en el singleton un método createEntityManager que devuelve un nuevo entity manager obtenido a partir de esta factoría. Definimos también el método setEntityManagerFactory para dar flexibilidad a la clase y poder inyectar en ella una nueva factoría de entity managers. De esta forma podríamos cambiar en tiempo de ejecución la unidad de persistencia con la que trabaja la aplicación.

package es.ua.jtech.jbib.persistence;

	import javax.persistence.EntityManager;
	import javax.persistence.EntityManagerFactory;
	import javax.persistence.Persistence;

	public class PersistenceManager {
	   static private final String PERSISTENCE_UNIT_NAME = "biblioteca";
	   protected static PersistenceManager me = null;
	   private EntityManagerFactory emf = null;

	   private PersistenceManager() {
	   }

	   public static PersistenceManager getInstance() {
	      if (me == null) {
	         me = new PersistenceManager();
	      }
	      return me;
	   }

	   public void setEntityManagerFactory(EntityManagerFactory myEmf) {
	      emf = myEmf;
	   }
	
	   public EntityManager createEntityManager() {
	      // Si el emf no se ha inicializado o se ha cerrado, se vuelve a abrir
	      if (emf == null) {
	         emf = Persistence
	               .createEntityManagerFactory(PERSISTENCE_UNIT_NAME);
	         this.setEntityManagerFactory(emf);
	      }
	      return emf.createEntityManager();
	   }
	
	   public void close() {
	      if (emf != null)
	         emf.close();
	         emf = null;
	   }
	}

Por último, crea el siguiente test en el directorio de tests que comprueba que el singleton funciona correctamente:

package es.ua.jtech.jbib.persistence;

import static org.junit.Assert.*;

import javax.persistence.EntityManager;
import org.junit.Test;

public class PersistenceManagerTest {
   @Test
   public void obtenerEntityManagerTest() {
      EntityManager em = PersistenceManager.getInstance().createEntityManager();
      em.getTransaction().begin();
      assertTrue(em.isOpen());
      em.getTransaction().commit();
      em.close();
      assertFalse(em.isOpen());
   }
}

Capa de lógica de negocio

Introducción

Una vez mapeadas las entidades y creados los DAO con los que recupararlas y actualizarlas crearemos en este apartado las clases con los métodos de servicio que permitan trabajar con la biblioteca:

  • Operaciones: listar libros disponibles, listar libros reservados, reservar libros, ...
  • CRUD de libros: creación, recuperación, actualización y borrado

Vamos ahora a definir las clases que implementan la lógica de negocio. Tal y como hemos comentado, estas clases de negocio son muy importantes, porque definen las funcionalidades que exportamos a otras capas, como la capa web. Los objetos que devuelven son objetos de dominio desconectados de la base de datos. Reciben como parámetro identificadores y datos elementales relacionados con la operación que se quiere realizar (un identificador de ejemplar, o un identificador de usuario por ejemplo). Las clases de negocio encapsulan toda la funcionalidad y de nuestra aplicación y proporcionan un front-end de la capa de persistencia.

Todos los métodos de las clases de negocio son transaccionales. Si el método termina correctamente estaremos seguros de que todas las actualizaciones se han realizado en la base de datos. Si algo falla en el método, se devolverá una excepción y se hará un rollback del estado de la base de datos, de forma que todo volverá al estado previo a la realización de la llamada al método.

La capa de negocio se implementará en un nuevo proyecto llamado jbib-negocio que dependerá del anterior jbib-modelo que es el que implementa la capa de persistencia.

Debido a la gran cantidad de código a desarrollar y al poco tiempo que tenemos para hacerlo, vamos a proporcionar un código mínimo y te vamos a pedir que amplíes muy pocas funcionalidades. Es importante que no sólo copies el código, sino que también lo leas y reflexiones sobre su funcionamiento y estructura. Más adelante iremos añadiendo nuevos métodos a la capa de negocio.

Proyecto jbib-negocio

Empezamos creando el nuevo proyecto jbib-negocio que va a contener todas las clases e interfaces relacionadas con la lógica de negocio.

En el espacio de trabajo del proyecto de integración crea un nuevo proyecto jbib-negocio. Escoge la opción de Eclipse New > Project... > Maven > Maven Module, marcando la opción de Create simple project e introduciendo:

  • Module Name: jbib-negocio
  • Parent Project: proyint-jbib
  • Name: jbib-negocio

Añade las siguientes dependencias al POM, necesarias para que funcionen correctamente las llamadas a la capa de datos:

   <dependencies>
      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-log4j12</artifactId>
         <version>1.6.1</version>
         <scope>runtime</scope>
      </dependency>

      <dependency>
         <groupId>es.ua.jtech.proyint</groupId>
         <artifactId>jbib-modelo</artifactId>
         <version>0.0.1-SNAPSHOT</version>
      </dependency>
   </dependencies>

Para probar que se accede correctamente a la capa de persistencia desde la capa de negocio, copiamos el test PersistenceManagerTest en este proyecto, en el directorio src/test/java.

Ejecutamos el test desde Eclipse y desde línea de comandos ejecutamos todos los tests de todos los proyectos, desde el proyecto padre:

$ cd proyint-expertojava/jbib
$ mvn test
...
[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary:
[INFO] 
[INFO] jbib .............................................. SUCCESS [0.097s]
[INFO] jbib-modelo ....................................... SUCCESS [6.499s]
[INFO] jbib-negocio ...................................... SUCCESS [2.159s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------

Capa de lógica de negocio

Vamos ahora a definir las clases que implementan la lógica de negocio. Tal y como hemos comentado, estas clases de negocio son muy importantes, porque definen las funcionalidades que exportamos a otras capas, como la capa web. Los objetos que devuelven son objetos de dominio desconectados de la base de datos. Reciben como parámetro identificadores y datos elementales relacionados con la operación que se quiere realizar (un identificador de ejemplar, o un identificador de usuario por ejemplo). Las clases de negocio encapsulan toda la funcionalidad y de nuestra aplicación y proporcionan un front-end de la capa de persistencia.

Todos los métodos de las clases de negocio son transaccionales. Si el método termina correctamente estaremos seguros de que todas las actualizaciones se han realizado en la base de datos. Si algo falla en el método, se devolverá una excepción y se hará un rollback del estado de la base de datos, de forma que todo volverá al estado previo a la realización de la llamada al método.

Utilizaremos el patrón factoría para definir las clases. Especificaremos las clases con interfaces y una factoría devolverá la implementación concreta. Esto nos permitirá en el futuro sustituir la implementación de la capa de negocio por otra que, por ejemplo, utilice EJBs.

En cuanto a notación, llamaremos a las clases de negocio con un nombre que agrupe los servicios que ofrecen y el Service:

  • OperacionService: va a permitir realizar operaciones como solicitar préstamo por un usuario, solicitar una reserva, etc.
  • LibroService: CRUD sobre libros y ejemplares, listados de consultas
  • UsuarioService: operaciones para recuperar y consultar usuarios

Interfaces de las clases de negocio

A continuación listamos las interfaces de las clases de negocio. Definen las funcionalidades que se ofrece a otras capas de la aplicación y que debemos implementar con las clases de negocio. Estas funcionalidades son las mínimas definidas. Más adelante añadiremos más funcionalidades conforme las vayamos necesitando.

Definimos todas las clases y todas las excepciones necesarias en el paquete es.ua.jtech.jbib.service. Todas las excepciones las debes crear de tipo RuntimeException.

Interfaz IOperacionService:

package es.ua.jtech.jbib.service;

import es.ua.jtech.jbib.model.EjemplarDomain;

public interface IOperacionService {
   /**
    * Realiza la petición de un prestamo de un usuario sobre un ejemplar. El
    * usuario tiene que ser un profesor o un alumno activo que no tiene cubierto
    * el cupo de préstamos. El ejemplar debe estar disponible.
    * 
    * @param usuarioId el id del usuario
    * @param ejemplarId el id del ejemplar
    * @return el ejemplar con los datos del prestamo: usuario y fecha de
    *         devolución
    * @throws OperacionServiceException en las siguientes condiciones:
    *            <ul>
    *               <li>el usuario o el ejemplar no existen</li>
    *               <li>el usuario no está activo</li>
    *               <li>el usuario tiene completo el cupo de préstamos</li>
    *               <li>el ejemplar no está disponible</li>
    *            </ul>
    */
   EjemplarDomain pidePrestamo(Long usuarioId, Long ejemplarId);
}

Vemos que la interfaz define el método pidePrestamo(usuarioId, ejemplarId) con el que se realiza un préstamo de un ejemplar a un usuario. La operación tiene un conjunto de condiciones que se deben cumplir para que funcione correctamente. En el caso en que alguna condición no se satisfaga, se lanza una excepción de tipo OperacionServiceException. Se trata de una excepción runtime, por lo que no hay que declararla con la cláusula throws en el perfil del método, pero sí que hay que documentarla.

La excepción OperacionServiceException es hija de BibliotecaException y se define de la siguiente forma:

package es.ua.jtech.jbib.service;

import es.ua.jtech.jbib.BibliotecaException;

public class OperacionServiceException extends BibliotecaException {

   private static final long serialVersionUID = 1L;

   public OperacionServiceException() {
      super();
  }
  
  public OperacionServiceException(String message) {
      super(message);
  }
  
  public OperacionServiceException(String message, Throwable cause) {
      super(message, cause);
  }
}

Vemos el resto de interfaces y los métodos que definen:

Interfaz ILibroService:

package es.ua.jtech.jbib.service;

import java.util.List;

import es.ua.jtech.jbib.model.EjemplarDomain;
import es.ua.jtech.jbib.model.LibroDomain;

public interface ILibroService {
   List<LibroDomain> listaLibros();

   /**
    * Recupera un libro de la biblioteca por su identificador. Junto con el
    * libro se recuperan también sus ejemplares. Si el libro no existe devuelve
    * null.
    * 
    * @param libroId el id del libro a recuperar
    * @return el libro o null
    */
   LibroDomain recuperaLibro(Long libroId);

   /**
    * Recupera un ejemplar de un libro de la biblioteca por su identificador.
    * 
    * @param ejemplarId el id del ejemplar a recuperar
    * @return el libro o null
    */
   EjemplarDomain recuperaEjemplar(Long ejemplarId);

   /**
    * Recupera un libro de la biblioteca por su ISBN. En el libro se recuperan
    * también sus ejemplares. Si el libro no existe devuelve null.
    * 
    * @param isbn el ISBN del libro a recuperar
    * @return el libro o null
    */
   LibroDomain buscaLibroPorIsbn(String isbn);

   /**
    * Busca todos los libros cuyo autor+título contengan una palabra.
    * 
    * @param keyword palabra a buscar
    * @return lista con todos los libros que contienen la palabra
    */
   public List<LibroDomain> buscaLibros(String keyword);

   /**
    * Devuelve una lista de todos los libros de la biblioteca, ordenados por
    * título.
    * 
    * @return la lista de libros
    */
   public List<LibroDomain> listaLibrosPorTitulo();

   /**
    * Devuelve una lista paginada de los libros de la biblioteca ordenados por
    * título, empezando por el número firstResult (siendo 0 el primero). Devuelve los
    * maxResults siguientes.
    * 
    * @param firstResult
    * @param maxResults
    * @return la lista de libros
    */
   public List<LibroDomain> listaLibrosPorTitulo(int firstResult,
         int maxResults);
}

Interfaz IUsuarioService:

package es.ua.jtech.jbib.service;

import es.ua.jtech.jbib.model.UsuarioDomain;

public interface IUsuarioService {

   /**
    * Recupera un usuario de la biblioteca por su identificador. Junto con el
    * usuario se recupera también su colección de libros prestados y reservados.
    * Si el usuario no existe devuelve null.
    * 
    * @param usuarioId el id del usuario a recuperar
    * @return el usuario o null
    */
   UsuarioDomain recuperaUsuario(Long idUsuario);

   /**
    * Busca un usuario de la biblioteca por su login. Junto con el usuario se
    * recupera también su colección de libros prestados y reservados. Si el usuario no
    * existe devuelve null.
    * 
    * @param login el login del usuario a recuperar
    * @return el usuario o null
    */
   UsuarioDomain buscaUsuarioPorLogin(String login);
}

Factoría de clases de negocio

La factoría de las clases de negocio permite obtener las implementaciones concretas LibroService, OperacionService y UsuarioService (todavía sin implementar):

package es.ua.jtech.jbib.service;

	public class FactoriaServices {

	   public static FactoriaServices me;

	   protected FactoriaServices() { }

	   public static FactoriaServices getInstance() {
	      if (me == null) {
	         me = new FactoriaServices();
	      }
	      return me;
	   }

	   public ILibroService getLibroService() {
	      return new LibroService();
	   }

	   public IOperacionService getOperacionService() {
	      return new OperacionService();
	   }

	   public IUsuarioService getUsuarioService() {
	      return new UsuarioService();
	   }
}

Consultas

Para implementar las funcionalidades de la capa de negocio, debemos ampliar los DAO del proyecto jbib-modelo con los siguientes métodos. También debemos añadir las consultas JPA asociadas en las clases entidad:

LibroDao:

  • LibroDomain findByIsbn(String isbn): Devuelve el libroDomain correspondiente al isbn que se le pasa como parámetro
  • List<LibroDomain> findAllLibrosByTitulo(): Devuelve todos los libros ordenados por título
  • List<LibroDomain> buscaLibros(String palabra): Devuelve todos los libros que contienen la palabra en el título o autor

UsuarioDao:

  • UsuarioDomain findByLogin(String login): Devuelve el usuarioDomain correspondiente al login que se le pasa como parámetro

Preparación de las pruebas con DbUnit

Utiliza también DbUnit para probar la capa de servicios.

Puedes usar como base este fichero de prueba que debes colocar en src/test/resources:

Fichero src/test/resources/test-service-dataset.xml

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
   <!-- Bibliotecarios -->
   <bibliotecario id="1" login="javier.huertas" password="11111"/>
    
   <!-- Usuarios -->
   <usuario tipo="PROFESOR" id="1" login="aitor" password="aitor"
      email="a@gmail.com" nombre="Aitor" apellido1="Medrano" apellido2="Escrig"
      departamento="Informatica" estado="ACTIVO" calle="Bartolome"
      numero="11" piso="5 B" ciudad="Helsinki" codigoPostal="12345" />
   <usuario tipo="ALUMNO" id="2" login="juan" password="antonio"
      email="juan@dececia.es" nombre="Juan Antonio" apellido1="Perez"
      apellido2="Perez" tutor="Aitor Medrano" estado="ACTIVO" calle="Almassera"
      numero="15" piso="1" ciudad="El Verger" codigoPostal="03770" />
      
   <!-- Libros -->
   <libro id="1" isbn="0131401572" titulo="Data Access Patterns"
      autor="Clifton Nock" numPaginas="512" fechaAlta="2007-02-22"
      numDisponibles="2" />
   <libro id="2" isbn="0321127420"
      titulo="Patterns Of Enterprise Application Architecture" autor="Martin Fowler"
      numPaginas="533" fechaAlta="2008-10-23" numDisponibles="1" />
   <libro id="3" isbn="0321180860" titulo="Understanding SOA with Web Services"
      autor="Eric Newcomer and Greg Lomow" numPaginas="465" fechaAlta="2007-01-25"
      numDisponibles="2" />

   <!-- Ejemplares de Data Access Patterns -->
   <ejemplar id="1" idEjemplar="001" fechaAdquisicion="2007-02-22"
      libro_id="1" localizacion="SALA" />
   <ejemplar id="2" idEjemplar="002" fechaAdquisicion="2007-02-22"
      libro_id="1" localizacion="SALA" />

   <!-- Ejemplares de Patterns Of Enterprise Application Architecture -->
   <!-- Prestado a Aitor Medrano -->
   <ejemplar id="3" idEjemplar="001" fechaAdquisicion="2008-10-23"
      libro_id="2" localizacion="DEPARTAMENTO" usuario_id="1" />
   <!-- Disponible -->
   <ejemplar id="4" idEjemplar="002" fechaAdquisicion="2008-10-23"
      libro_id="2" localizacion="SALA" />

   <!-- Ejemplares de Understanding SOA with Web Services -->
   <ejemplar id="5" idEjemplar="001" fechaAdquisicion="2007-01-25"
      libro_id="3" localizacion="SALA" />
   <ejemplar id="6" idEjemplar="002" fechaAdquisicion="2007-01-25"
      libro_id="3" localizacion="SALA" />

    <!-- Insertamos un elemento de cada una de las tablas restantes
         para asegurarnos de que la base de datos se inicializa correctamente (DBUnit
         sólo borra las tablas que aparecen en el dataset) -->

    <reserva/>
    <historico/>
    <multa/>

</dataset>

Define en src/test/java/ el test es.ua.jtech.jbib.service.ServiceTest.java en el que se cargue el anterior dataset. Para ello define los métodos initDatabaseTest(), closeEntityManagerFactory() y cleanDB() como hicimos en el proyecto jbib-modelo.

Añade algunos tests que comprueben los métodos de negocio. Por ejemplo el siguiente comprueba que el listado de todos los libros debe tener tres elementos. Obtenemos el libroService a partir de la factoría y llamamos a su método listaLibros:

  @Test
  public void listaLibrosTest() {
     ILibroService libroService = FactoriaServices.getInstance().getLibroService();
     List<LibroDomain> libros = libroService.listaLibros();
     assertTrue(libros.size()==3);
  }

El siguiente test comprueba un préstamo de un ejemplar:

  @Test
   public void operacionServicePidePrestamoTest() {
      IOperacionService operacionService = FactoriaServices.getInstance()
                                              .getOperacionService();
      ILibroService libroService = FactoriaServices.getInstance().getLibroService();    
      Long ejemplarId = 5L;
      Long usuarioId = 2L;
      operacionService.pidePrestamo(usuarioId, ejemplarId);
      EjemplarDomain ejemplar = libroService.recuperaEjemplar(ejemplarId);
      LibroDomain libro = libroService.recuperaLibro(ejemplar.getLibro().getId());
      assertTrue(libro.getNumDisponibles().equals(1));
      assertTrue(ejemplar.getUsuario().getId().equals(usuarioId));
   }

Ahora el trabajo que falta es implementar los servicios para que estos tests funcionen correctamente.

Implementación de los servicios

Terminamos con un ejemplo de implementación de un método de LibroService:

package es.ua.jtech.jbib.service;

// imports

public class LibroService implements ILibroService {

   @Override
   public List<LibroDomain> listaLibros() {
      EntityManager em = PersistenceManager.getInstance()
            .createEntityManager();
      em.getTransaction().begin();

      LibroDao libroDao = new LibroDao(em);
      List<LibroDomain> lista = libroDao.getAllLibros();

      em.getTransaction().commit();
      em.close();
      return lista;
   }

   @Override
   public LibroDomain recuperaLibro(Long libroId) {
      EntityManager em = PersistenceManager.getInstance()
            .createEntityManager();
      em.getTransaction().begin();

      LibroDao libroDao = new LibroDao(em);
      LibroDomain libro = libroDao.find(libroId);
      if (libro != null) {
         // accedemos a la lista de ejemplares para cargarlos en memoria
         libro.getEjemplares().size();
      }
      em.getTransaction().commit();
      em.close();
      return libro;
   }

   @Override
   public LibroDomain buscaLibroPorIsbn(String isbn) {
      // TODO Auto-generated method stub
      return null;
   }

   @Override
   public List<LibroDomain> buscaLibros(String keyword) {
      // TODO Auto-generated method stub
      return null;
   }

   @Override
   public List<LibroDomain> listaLibrosPorTitulo() {
      // TODO Auto-generated method stub
      return null;
   }

   @Override
   public List<LibroDomain> listaLibrosPorTitulo(int firstResult,
         int maxResults) {
      // TODO Auto-generated method stub
      return null;
   }

   @Override
   public EjemplarDomain recuperaEjemplar(Long ejemplarId) {
      // TODO Auto-generated method stub
      return null;
   }
}

Algunos comentarios sobre los métodos en la capa de negocio:

  • Gestionamos correctamente la transacción y el entity manager: cerramos siempre el entity manager poniéndolo en el finally y hacemos un rollback de la transacción si hay algún error
  • En todos los métodos comprobamos que se cumplen las precondiciones y lanzamos una excepción en el caso en que no sea así
  • Los argumentos que se toman como parámetros son datos sencillos (el identificador de un usuario y el isbn de un libro, por ejemplo) y se devuelven objetos de dominio desconectados del contexto de persistencia
  • Hay que tener cuidado en cargar en memoria todos los objetos que vayan a necesitar los que llamen al método. Los atributos lazy del objeto devuelto no van a estar disponibles, porque el objeto está desconectado del entity manager.

Y también listamos la clase OperacionService:

package es.ua.jtech.jbib.service;

// imports

public class OperacionService implements IOperacionService {
   private Log logger = LogFactory.getLog(OperacionService.class);

   @Override
   public EjemplarDomain pidePrestamo(Long usuarioId, Long ejemplarId) {
      EntityManager em = PersistenceManager.getInstance()
            .createEntityManager();
      EntityTransaction tx = em.getTransaction();

      try {
         tx.begin();
         UsuarioDao usuarioDao = new UsuarioDao(em);
         LibroDao libroDao = new LibroDao(em);
         EjemplarDao ejemplarDao = new EjemplarDao(em);

         UsuarioDomain usuario = usuarioDao.find(usuarioId);
         EjemplarDomain ejemplar = ejemplarDao.find(ejemplarId);

         // Comprobamos que el ejemplar y el usuario existen

         if (usuario == null)
            throw new OperacionServiceException(
                  "El usuario especificado no existe");

         if (ejemplar == null)
            throw new OperacionServiceException(
                  "El ejemplar especificado no existe");

         // Comprobamos que el usuario sea activo
         
         if (usuario.getEstado() != EstadoUsuario.ACTIVO)
            throw new OperacionServiceException("El usuario no está ACTIVO");

         // Comprobamos que el ejemplar está disponible para ser prestado

         if (ejemplar.getEstado() != EstadoEjemplar.DISPONIBLE)
            throw new OperacionServiceException("El ejemplar no está disponible");
         
         // Comprobamos que el usuario no tiene lleno el cupo de operaciones

         int numOp = usuario.getPrestamos().size() + usuario.getReservas().size() + 1;
         BibliotecaBR.getInstance().compruebaCupoOperaciones(usuario, numOp);
         
         // Realizamos el préstamo actualizando las entidades

         ejemplar.setUsuario(usuario);
         ejemplar.getLibro().setNumDisponibles(
               ejemplar.getLibro().getNumDisponibles() - 1);

        // Calcula fecha de devolucion

         int dias = BibliotecaBR.getInstance().calculaNumDiasPrestamo(usuario);
         Calendar calendar = Calendar.getInstance();
         ejemplar.setFechaPrestamo(calendar.getTime());
         calendar.add(Calendar.DATE, dias);
         ejemplar.setFechaDevolucion(calendar.getTime());

         libroDao.update(ejemplar.getLibro());
         ejemplarDao.update(ejemplar);
         tx.commit();

         return ejemplar;
      } catch (RuntimeException ex) {
         try {
            tx.rollback();
         } catch (RuntimeException rbEx) {
            logger.error("No se ha podido deshacer la transacción",
                  rbEx);
         }
         throw ex;
      } finally {
          em.close();
      }
   }
}

Javadoc

Es muy sencillo crear la documentación Javadoc del proyecto utilizando el pluging javadoc de Maven:

$ cd jbib
$ mvn javadoc:javadoc

La documentación javadoc se genera en el directorio target/site/apidocs de cada proyecto:

Resumen

Además de construir los proyectos jbib-modelo y jbib-negocio con las clases anteriores, debes realizar lo siguiente:

  • Terminar de implementar la clase LibroService con los métodos definidos en la interfaz
  • Implementar la clase UsuarioService con los métodos definidos en la interfaz
  • Comprobar el funcionamiento correcto de las clases Service con un conjunto de tests

Las siguientes imágenes muestran todos los archivos que debe contener el proyecto Eclipse jbib-modelo:

Y las siguientes muestran el proyecto jbib-negocio:

Para la entrega se deberá etiquetar repositorio bitbucket con el tag entrega-proyint-negocio.