Mejorando el uso de Hibernate en nuestra aplicación PrimeFaces/JSF/Spring

 

Hola Estimad@s,


en esta ocasión intentaremos mejorar el uso de Hibernate en nuestra aplicación web.

Objetivos

  • Utilizar una versión mejorada de HibernateUtil (fuente
  • Crear una sesión Hibernate por petición web y encapsularla en su propio hilo para ser coherentes con lo requerido por la documentación oficial y poder implementar el patrón Open Session In View
  • Unificar y simplificar el tratamiento de excepciones (fuente)
  • Implementar el patrón DAO 
  Desarrollo
En esta instancia no descargaremos más librerías, ya tenemos todas las necesarias para seguir adelante. Si utilizaremos nuevas clases, muchas de las cuales no se explicarán en profundidad porque pertenecen a otro curso/tutorial el cual está muy bien explicado y desarrollado con mucho detalle. Las clases base que utilizaremos son:   ar/com/magm/persistencia/hibernate/util/GenericEventListenerImpl.java ar/com/magm/persistencia/hibernate/util/GenericIntegratorImpl.java ar/com/magm/persistencia/hibernate/util/HibernateUtil.java ar/com/magm/persistencia/hibernate/util/SessionFactoryImplThreadLocal.java ar/com/magm/persistencia/hibernate/util/HibernateUtilInternalState.java

Las clases anteriores contienen la lógica para crear sesiones Hibernate parametrizables y proporciona ayuda para implementar el patrón Open Session In View.


ar/com/magm/persistencia/dao/GenericDAO.java

Interface que define las operaciones de persistencia para los DAO.


ar/com/magm/persistencia/dao/hibernateimpl/GenericDAOImplHibernate.java Implementación DAO que utiliza Hibernate
  ar/com/magm/persistencia/exception/Caption.java ar/com/magm/persistencia/exception/BussinessException.java ar/com/magm/persistencia/exception/BussinessMessage.java

Clases que permiten Simplificar y Unificar el manejo de Excepciones, de hecho solo se utilizará BussinessException sin por ello perder el origen de las excepciones.


ar/com/magm/persistencia/hibernate/web/HibernateContextListenerAndFilter.java

Clase que es un WebLIstener y un WebFilter a la vez.
El WebListener se encarga al inicio de la aplicación de inicializar la fábrica de sesiones de Hibernate y al final la cierra.
El WebFilter asegura que exista una sesión Hibernate en un hilo local por petición, de esta forma se puede implementar el patrón Open Session In View.


Y se encuentran en este archivo:
https://github.com/magm3333/workspace-pftuto/blob/master/paquetePersistencia.zip?raw=true

  Además crearemos una serie de clases propias de la aplicación.

Implementación del patrón Data Access Object (DAO)

El patrón DAO nos permite crear una capa de abstracción entre nuestros objetos y algún sistema de almacenamiento.
Se trata de declarar las operaciones de persistencia que podremos realizar con nuestros objetos y luego tener las implementaciones particulares que deseemos. Trabajar con Spring nos ayudará a realizar la última tarea ya que mediante la inyección de dependencias nos abstraemos del problema de la implementación particular.

Comenzaremos creando la interface GenericDAO, la cual contendrá las operaciones básicas comunes a nuestros DAO, el código es el siguiente:

package ar.com.magm.persistencia.dao;

import java.io.Serializable;
import java.util.List;
import ar.com.magm.persistencia.exception.BussinessException;

public interface GenericDAO<T, ID extends Serializable> {
T create() throws BussinessException;
void saveOrUpdate(T entity) throws BussinessException;
T get(ID id) throws BussinessException;
void delete(ID id) throws BussinessException;
List<T> findAll() throws BussinessException;
}
Como se observa las operaciones son 5:

create: creará una nueva instancia de una entidad que puede persistirse.
saveOrUpdateasegura la persistencia de una entidad, si no existe la inserta y si existe la actualiza
get: obtiene una instancia de una entidad desde el sistema de almacenamiento por medio de su clave.
delete: elimina una entidad por medio de su clave.
findAll: obtiene todas las entidades de un tipo desde el almacenamiento persistente.

Como puede observarse, tanto la entidad como su identificador son genéricos, en el próximo paso crearemos  las versiones particulares para Cliente y Zona

ClienteDAO.java


package ar.com.magm.persistencia.dao;
import ar.com.magm.model.Cliente;
public interface ClienteDAO extends GenericDAO<Cliente, Integer> {
}
  ZonaDAO.java

package ar.com.magm.persistencia.dao;
import ar.com.magm.model.Zona;
public interface ZonaDAO extends GenericDAO<Zona, Integer> {
}
Bien simple las versiones particulares no?
Para completar, nos resta realizar al menos una implementación y será para Hibernate.

Implementación DAO para Hibernate

Ya existe una clase llamada ar.com.magm.persistencia.dao.hibernateimpl.GenericDAOImplHibernate, la cual contiene la implementación de los métodos genéricos definidos en GenericDAO, el análisis de esta clase escapa a este tutorial y además ya existe una buena explicación aquí, nos remitiremos solo a crear las implementaciones particulares para ClienteDAO y ZonaDAO.

ClienteDAOImplHibernate.java

package ar.com.magm.persistencia.dao.hibernateimpl;
import ar.com.magm.model.Cliente;
import ar.com.magm.persistencia.dao.ClienteDAO;
public class ClienteDAOImplHibernate extends
GenericDAOImplHibernate<Cliente, Integer> implements ClienteDAO {
}

ZonaDAOImplHibernate.java

package ar.com.magm.persistencia.dao.hibernateimpl; import ar.com.magm.model.Zona;
import ar.com.magm.persistencia.dao.ZonaDAO;
public class ZonaDAOImplHibernate extends
GenericDAOImplHibernate<Zona, Integer> implements ZonaDAO {
}  
Buen enfoque el uso de Genéricos no?, teniendo una buena implemetación genérica, las implementaciones particulares son triviales.

Usando Spring para independizarnos de la implementación

Ya tenemos nuestros DAO definidos y una implementación particular para Hibernate, pero podríamos tener cuantas implementaciones deseemos, si ese fuera el caso, sería muy bueno no tener que reescribir clases ni recompilarlas cuando cambiemos de implementación, aquí es donde Spring "nos da una mano".
A continuación crearemos un controlador que procesará peticiones requiriendo información de clientes, en futuros tutoriales formalizaremos el concepto de controlador y veremos como utilizar el framework Modelo Vista Controlador(MVC) de Spring.

El controlador

ClienteTestController.java


package ar.com.magm.model.dao.controller;
import java.io.IOException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import ar.com.magm.model.Cliente;
import ar.com.magm.persistencia.dao.ClienteDAO;
import ar.com.magm.persistencia.exception.BussinessException;

@Component("clienteController")
@Scope("request")
public class ClienteTestController {
  @Autowired
  private ClienteDAO clienteDAO;

  public void processRequest(HttpServletRequest request,
                             HttpServletResponse response) throws IOException {
    String idCl = request.getParameter("idCliente");
    int idCliente = 33;
    if (idCl != null)
      idCliente = Integer.parseInt(idCl);
    String salida;
    try {
      Cliente cl = clienteDAO.get(idCliente);
      StringBuilder str = new StringBuilder();
      if (cl != null) {
        str.append("Cliente: " + cl.getCliente() + " (Id: "
             + cl.getIdCliente() + ") - Cta Habilitada: "
        str.append("\tZona: " + cl.getZona().getZona() + " (Id: "
             + cl.getZona().getIdZona() + ")");
      } else {
        str.append("No existe el cliente con id=" + idCliente);
      }
      salida = str.toString();
    } catch (BussinessException ex) {
      salida = "Error el obtener el cliente\n" + ex.getMessage();
    }
    response.getWriter().print(salida);
    response.getWriter().flush();
  }
}

Esta clase controlador es un bean manejado por Spring, solo que no se ha utilizado el archivo xml (applicationContext.xml) para configurarlo, en vez de ello se han utilizado anotaciones, en particular:

@Component("clienteController")
@Scope("request")
sería lo mismo que colocar:
<bean id="clienteController" class="ar.com.magm.model.dao.controller.ClienteTestController" scope="request"/>el archivo xml (applicationContext.xml).
El scope = "request" indica que se creará una instancia del controlador por cada petición del cliente, es un tiempo de vida muy corto y solo dura lo que dura la petición.

Respecto a la línea anotada con:

@Autowired
private ClienteDAO clienteDAO;

si analizamos la clase con detenimiento, podremos notar que nunca se instancia de forma explícita (new), esta es la línea que nos abstrae de la implementación, será Spring el encargado de inyectar aquí el código (o dependencia) correcto, Spring buscará alguna clase que implemente alguna clase que implemente la interface ClienteDAO, más adelante, en el archivo de configuración (applicationContext.xml), le diremos a Spring cuales son las posibilidades que tiene, en otras palabras, cuales son las clases que implementan esta interfaz.
También notemos que con solo Cliente cl = clienteDAO.get(idCliente); obtenemos la instancia de la entidad Cliente que deseamos.
Podemos ver el manejo simplificado de excepciones, solo capturamos BussinessException, sin embargo siempre tendremos la posibilidad de obtener toda la información de la excepción particular cuando así lo deseemos.

Configurando la detección de anotaciones y los beans candidatos para la inyección de dependencias

Como he adelantado, debemos realizar un par de tareas de configuración en el archivo applicationContext.xml.


<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="https://www.springframework.org/schema/beans"
       xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="https://www.springframework.org/schema/context"
       xsi:schemaLocation="https://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans-3.0.xsd
          https://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context-3.0.xsd">

  <context:annotation-config/>
  <context:component-scan base-package="ar.com.magm.model.dao.controller"/>
    
  <bean class="ar.com.magm.persistencia.dao.hibernateimpl.ClienteDAOImplHibernate" />
  <bean class="ar.com.magm.persistencia.dao.hibernateimpl.ZonaDAOImplHibernate" />

  <bean id="gaugeBean" class="ar.com.magm.web.primefaces.GaugeBean" scope="singleton"/>
  <bean id="ventasBean" class="ar.com.magm.web.primefaces.VentasBean" scope="session"/>
  <bean id="loginBean" class="ar.com.magm.web.primefaces.LoginBean" scope="session"/>
</beans>
La línea <context:annotation-config/>, habilita el uso de anotaciones en Spring, con esta línea podemos usar anotaciones como @Autowired
Con la línea<context:component-scan base-package="ar.com.magm.model.dao.controller"/>
le indicamos a Spring a partir de que jerarquía de paquete deberá buscar los componentes marcados con  la anotación @Component Por último las líneas <bean class="ar.com.magm.persistencia.dao.hibernateimpl.ClienteDAOImplHibernate" />
<bean class="ar.com.magm.persistencia.dao.hibernateimpl.ZonaDAOImplHibernate" />
Hacen que Spring cree una instancia de cada implementación particular de DAO para Hibernate, uno de estos beans será inyectado en:
@Autowired
private ClienteDAO clienteDAO;
Por ello, si quisiéramos  cambiar de implementación, solo deberíamos modificar estas líneas y agregar las nuevas implementaciones al classpath.

Una prueba inyteresante podría ser agregar la siguiente línea a ClienteTestController:
...
Cliente cl = clienteDAO.get(idCliente);
System.out.println(clienteDAO.getClass().toString());
StringBuilder str = new StringBuilder();
...


Si lo ejecutamos veremos en la consola la siguiente salida:
class ar.com.magm.persistencia.dao.hibernateimpl.ClienteDAOImplHibernate

Con esto queda confirmado que la inyección de dependencia funciona correctamente.

Un servlet para probar que todo funcione

Crearemos ahora un servlet llamado TestHibernateConSpring, nos servirá, como el anterior, para probar que todo funcione correctamente. El código del servlet es el siguiente:


package ar.com.magm.web.servlet;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import ar.com.magm.model.dao.controller.ClienteTestController;

@WebServlet("/TestHibernateConSpring")
public class TestHibernateConSpring extends HttpServlet {
  protected void doGet(HttpServletRequest request,
         HttpServletResponse response) throws ServletException, IOException {

    ApplicationContext applicationContext = WebApplicationContextUtils
       .getWebApplicationContext(this.getServletContext());
    ClienteTestController controller = (ClienteTestController) applicationContext
       .getBean("clienteController");
    controller.processRequest(request, response);

  }

}


En las líneas
ApplicationContext applicationContext = WebApplicationContextUtils
   .getWebApplicationContext(this.getServletContext());
ClienteTestController controller = (ClienteTestController) applicationContext
   .getBean("clienteController");
Obtenemos la instancia del controlador, recordemos que se crea una instancia por petición, por ello podemos decir que esta instancia es exclusiva para este requerimiento.

Luego la línea
controller.processRequest(request, response);
le cede el control a la instancia del controlador, de esta forma organizamos un poco mejor nuestra aplicación  aunque todavía falta un tramo por recorrer.   Creando una sesión Hibernate cuando se requiere   Aún nos resta una cosa para poder probar nuestra aplicación. Modificaremos la clase: ar/com/magm/persistencia/hibernate/web/HibernateContextListenerAndFilter.java

La cual hemos explicado brevemente al comienzo de este texto.

Esta clase actúa entre otras cosas como filtro, los filtros se ejecutan antes que cualquier componente en un requerimiento, por otro lado se debe definir ante que requerimientos se debe ejecutar el filtro, esto se hace mediante la definición de patrones de URL. Esto hará que ante algunas URLs en particular se ejecute el filtro y ante otras no, por ejemplo no sería conveniente que este filtro cree sesiones Hibernate cuando la URL requiere algún recurso estático, por ejemplo un archivo .jpg o .gif. La modificación que debemos realizar es: antes: @WebFilter(urlPatterns = { "*.html" }) ahora: @WebFilter(urlPatterns = { "/TestHibernateConSpring" }) De esta forma solo se crearán recursos necesarios para la persistencia cuando sea necesario y no en otro momento, de esta forma ahorramos recursos en el server. En el futuro modificaremos nuevamente este componente.   Probando   Ahora vamos a nuestro navegador y con la url que ya hemos utilizado colocamos los datos de login:   Imagen eliminada. Presionamos el botón "Ingreso" y cuando nos encontremos en la pantalla principal:   Imagen eliminada.  
Cambiamos la url por: https://localhost:8080/pf/TestHibernateConSpring, presionamos enter y veremos un resultado similar al siguiente:

Imagen eliminada.   El cliente id=33 es el cliente por defecto (puede verse en el código del controlador) para cambiar el cliente que queremos ver debemos enviar un parámetro GET llamado idCliente, por ejemplo: Cambiamos la url por: https://localhost:8080/pf/TestHibernateConSpring?idCliente=12, veremos lo siguiente: Imagen eliminada.   Si requerimos un id que no existe veremos lo siguiente: Imagen eliminada.   Esto es todo por ahora.   Como siempre el proyecto completo y el WAR en el repo de GITHUB: https://github.com/magm3333/workspace-pftuto   Espero les sea de utilidad.     Saludos   Mariano