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
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
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.
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;
}
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.
saveOrUpdate: asegura 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> {
}
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> {
}
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="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://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>
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="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/context http://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.
<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:
Presionamos el botón "Ingreso" y cuando nos encontremos en la pantalla principal:
Cambiamos la url por: http://localhost:8080/pf/TestHibernateConSpring, presionamos enter y veremos un resultado similar al siguiente:
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: http://localhost:8080/pf/TestHibernateConSpring?idCliente=12, veremos lo siguiente:
Si requerimos un id que no existe veremos lo siguiente:
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
No hay comentarios.:
Publicar un comentario