Embarcado en el desarrollo del servicio de workflow de OCAS y en su implementación con JBPM me he tropezado (de nuevo) con un iceberg: las sesiones de Hibernate y el cómodo/incómodo lazy loading. Existe en javahispano un estupendo artículo de Martín sobre las sesiones y las transacciones de Hibernate que recomiendo a todo el mundo; pero intentaré resumir aquí cuál es el problema (a ver si lo consigo).
El lazy loading o carga perezosa es un mecanismo que hibernate implementa para tratar algunas relaciones. Una entidad (POJO que se persiste en una tabla de la BD) puede tener múltiples relaciones con otras entidades (por ejemplo, un producto puede tener una Collection de los comerciales que lo venden). A veces cuando nos traemos datos de la BD no nos interesa cargar toda la información relacionada ya que sólo vamos a usar o mostrar una parte. Así que, siguiendo con el ejemplo, si queremos mostrar el número de referencia de un producto y su nombre en una interfaz de usuario, cargar los datos de los veinte comerciales que lo venden en memoria ocupa espacio innecesariamente.
Usando el lazy loading, si definimos una relación como lazy (poniendo el atributo de la relación lazy a true en los mappings de hibernate), Sólo se cargará la información de los vendedores CUANDO accedamos a la Collection vendedores. Así por ejemplo:
producto.getVendedores(2).getName();
lo que provocará es un nuevo acceso a la base de datos para cargar la información de los vendedores. Llamarlo carga perezosa obedece a que no hemos tenido que escribir las líneas de código para traerlo de la base de datos: hasta que no hagamos uso de esa relación no se cargará en memoria, y cuando hagamos uso de ella “automágicamente” se traerá la información desde la base de datos. Por tanto para el programador resulta un mecanismo muy cómodo que permite realizar aplicaciones que aprovechan muy bien la memoria, sin cargar datos innecesarios hasta el momento que son necesarios.
EL PRECIO DEL LAZY LOADING
Claro está que cuando he escrito automágicamente nadie habrá pensado en Gandalf trayendo nuestros datos. Lo que hace hibernate es crear una clase wrapper de nuestra Collection y asociarla al objeto (por ejemplo un Set de vendedores). Estas clases wrapper actúan como un proxy: son clases herederas de las correspondientes Collection cuyos métodos están sobreescritos por los chicos de hibernate y lo que hacen es traer de la base de datos la entidad correspondiente. Pero para traer algo de una base de datos en Hibernate ES NECESARIO que la misma sesión de Hibernate en la que te has cargado el objeto original esté abierta. Y no se te ocurra pedir a los de hibernate que se pueda hacer abriendo una nueva sesión (sino quieres que se mofen de ti) porque como explican aquí se cargarían el concepto de transacción.
Hibernate sólo puede asegurar que el objeto original existe dentro de la misma transacción/sesión, por lo que si abres una nueva sesión deberás volver a cargar el objeto original y luego su Collection de entidades relacionadas.
Y aquí es cuando empiezan los problemas ;-)
ERRORES DE NOVATO EN HIBERNATE
Hace un año, para un proyecto que me tocó decidí utilizar hibernate para matar dos pájaros de un tiro (el proyecto era pequeño y así yo aprendía a usar hibernate). Tranquilos, pagué mi audacia con muchas horas de lectura de manuales y artículos :-D
Todo ufano cree un montón de relaciones lazy porque así mi aplicación web iba a ser la leche en verso (sólo en memoria lo que necesitara). Cuando llegué a mi primer jsp en el que además de datos de un producto de un almacén debía mostrar el nombre de su encargado (relación lazy por supuesto, ya que el encargado igual llevaba 700 piezas) me saltó la bonita LazyLoadingException. Vale, oh que tonto soy, el nombre del encargado viene a través de una carga perezosa y yo he cerrado la sesión de Hibernate al traer el producto. Pero…. glups!, si el único sitio en el que hago producto.getEncargado().getNombre() es en la página JSP!! Y yo abro la sesión en mi DAO y allí debería cerrarla (como chico aplicado y cumplidor que soy ;-)). ¿Que hago? ¿Le paso a la página jsp la sesión para que la cierre en cuanto me traiga el nombre del encargado? ¿Me tiro en postura fetal y espero que Dios arregle mi código? :-D
Tras secar el sudor frío de mi frente, me agencié “Hibernate in Action” el libro escrito por los desarrolladores de Hibernate y me puse a indagar cómo solucionaban ellos este problema.
Patrones, anti-patrones y opiniones
Vale, pues en Hibernate 2.1 (luego hablamos de la 3 que esto va para largo) explican claramente los distintos patrones que hay (y que Martín explica excelentemente en su artículo, leedlo!), recomendando el patrón session-per-request: por cada petición del cliente se abre una sesión, se trae de la BD lo que se tenga que traer y se cierra la sesión.
El tema está en ¿cómo lo implementamos en una aplicación web? Bueno pues en Hibernate 2.1 proponen una clase (HibernateUtil) que usando variables ThreadLocal (explicación de las poco conocidas variables ThreadLocal ) consiguen la magia, haciendo que cada petición (que en una aplicación web genera un thread) tenga a su disposición la creación de sesiones. Un pequeño resumen de la clase a continuación:
public class HibernateUtil {
private static Configuration configuration;
private static SessionFactory sessionFactory;
private static final ThreadLocal threadSession = new ThreadLocal();
private static final ThreadLocal threadTransaction = new ThreadLocal();
private static final ThreadLocal threadInterceptor = new ThreadLocal();
(...)
/**
* Retrieves the current Session local to the thread.
*
* If no Session is open, opens a new Session for the running thread.
*
* @return Session
*/
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
if (getInterceptor() != null) {
s = getSessionFactory().openSession(getInterceptor());
} else {
s = getSessionFactory().openSession();
}
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
/**
* Closes the Session local to the thread.
*/
public static void closeSession()throws InfrastructureException {
try {
Session s = (Session) threadSession.get();
threadSession.set(null);
if (s != null && s.isOpen()) {
s.close();
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
}
(...)
/**
* Register a Hibernate interceptor with the current thread.
*
* Every Session opened is opened with this interceptor after
* registration. Has no effect if the current Session of the
* thread is already open, effective on next close()/getSession().
*/
public static void registerInterceptor(Interceptor interceptor) {
threadInterceptor.set(interceptor);
}
private static Interceptor getInterceptor() {
Interceptor interceptor = (Interceptor) threadInterceptor.get();
return interceptor;
}
No he posteado la creación de la SessionFactory (se crea en un bloque de código estático), pero creo que el código se explica sólo ;-) Con este código es posible definir un Interceptor en Hibernate, que será llamado al abrir o cerrar una sesión, para hacer lo que queramos en esos eventos.
Y un pequeño ejemplo de código donde se usa (por ejemplo dentro de un DAO):
HibernateUtil.beginTransaction()
Query query = HibernateUtil.getSession().getNamedQuery("query1");
result = query.setEntity("usuario", usuario).list();
return result;
Uopss!! ¿Y donde has dejado el closeTransaction y el closeSession? Pues como decía antes, NO puedo cerrar la sesión en el DAO porque eso me quitaría la posibilidad de acceder a las relaciones lazy desde mi página jsp. Según el patrón sesion-per-request, la sesión se cerrará cuando toda la información se haya enviado y dibujado en la interfaz del usuario. Y en una aplicación web el sitio para hacerlo es un filtro de Servlet, que se ejecuta cuando la página jsp ya ha terminado de dibujar la pantalla. Como muestra un botón:
public class HibernateFilter implements Filter {
private static Log log = LogFactory.getLog(HibernateFilter.class);
public void init(FilterConfig filterConfig) throws ServletException {
log.info("Servlet filter init, now opening/closing a Session for each request.");
}
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// There is actually no explicit "opening" of a Session, the
// first call to HibernateUtil.beginTransaction() in control
// logic (e.g. use case controller/event handler) will get
// a fresh Session.
try {
chain.doFilter(request, response);
// Commit any pending database transaction.
HibernateUtil.commitTransaction();
} finally {
// No matter what happens, close the Session.
HibernateUtil.closeSession();
}
}
De esta forma cuando se haya dibujado la página jsp, se realizará un commit de todos los cambios a la base de datos y se cerraran la sesión que haya abierto el cliente (una sesión por petición).
Existe otra variante que en lugar de cerrar sesiones, las conecta y las desconecta (de la conexión JDBC subyacente).
Esta forma de trabajo, en la que abrimos sesiones en un lado y las cerramos en otro no acabó de convencer a todo el mundo, ni a mí, ya que hace a tu aplicación dependiente de Hibernate, no sólo en los DAO’s sino incluso en la capa de la aplicación web. Con Hibernate 3 el modelo cambió.
HIBERNATE 3: Donde dije Diego..
Como aparece aquí , todo ha cambiado en Hibernate 3, en el que han ampliado la clase SessionFactory con el método getCurrentSession(). Así que ahora podemos tener:
try {
factory.getCurrentSession().beginTransaction();
// Do some work
factory.getCurrentSession().load(...);
factory.getCurrentSession().persist(...);
factory.getCurrentSession().getTransaction().commit();
}
catch (RuntimeException e) {
factory.getCurrentSession().getTransaction().rollback();
throw e; // or display error message
}
No he mirado el código (el tiempo, ese escaso bien) pero “huele” bastante a que la implementación de ese método usará ThreadLocal internamente como el HibernateUtil.
Para librarnos de abrir y cerrar transacciones, de nuevo el filtro de servlet. Al ejemplo puesto antes ahora lo llaman patrón Open Session in View (¿verdad que es chulo llamar patrón de diseño a toda solución? ;-)) y está descrito aquí . La discusión de porque no crea una “dependencia real” (sic) con Hibernate aquilicuá . Que cada uno saque sus conclusiones, pero ya os aviso del tono de los Hibernate Boys: “si no estás de acuerdo conmigo es porque no tienes ni pajolera idea”. Aunque a veces resulta divertido escuchar a un prepotente, a la larga cansa :-D
En Hibernate 3, y como cada vez se acercan más al estándar EJB 3.0 (¿porqué será? ;-)), también existe la posibilidad de marcar las transacciones no sólo con el filtro de servlet, sino también con JTA. Así definiremos en un descriptor de despliegue (EJB2) o con annotations (EJB3) que determinados métodos que llamamos, requieren el inicio de una transacción, así nuestro código quedará más “bonito”:
// Do some work
factory.getCurrentSession().load(...);
factory.getCurrentSession().persist(...);
Y tendremos una sesión por transacción (tal y como recomiendan en el patrón sesion-per-request).
Recapitulando:
- O usamos JTA, en un entorno EJB
- O usamos el open session in View, con un filtro de servlet que nos abra transacciones.
Y en eso que llega la “primavera” y a todos la sangre altera ;-)
SPRING/HIBERNATE
Los desarrolladores de Spring en su afan de hacer la vida más fácil a los programadores proporcionaban desde Hibernate 2 unas clases de apoyo para integrar Hibernate en una filosofía IoC. EL SessionFactory de Hibernate se define como un bean en el contexto de la aplicación. Usando XML el bean se define así:
product.hbm.xml
net.sf.hibernate.dialect.MySQLDialect
Y para que nuestros DAO’s tengan acceso a ese SessionFactory:
…
Así, el código quedaría:
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
Primera gran diferencia: NO necesitamos HibernateUtil. Es más en el manual de referencia lo indican:
We strongly recommend such an instance-based setup over the old-school static HibernateUtil class from Hibernate’s CaveatEmptor sample application! (In general, do not keep any resources in static variables unless absolutely necessary!)
Toma torpedo a la línea de flotación de Gavin King. Y claro luego veremos como se toma las cosas el Gavin ;-D
Para acceder a las sesiones (y por tanto empezar a usar las posibilidades de hibernate) ofrecen varias alternativas:
- Usar la clase HibernateTemplate, que además de tener implementados ciertos métodos útiles (find, load, saveOrUpdate, o delete) permite “callback implementation” (una forma de realizar los punteros a funciones en Java, info ):
public Collection loadProductsByCategory(final String category) throws DataAccessException {
HibernateTemplate ht = new HibernateTemplate(this.sessionFactory);
return (Collection) ht.execute(new HibernateCallback() {
public Object doInHibernate(Session session) throws HibernateException {
Query query = session.createQuery("from test.Product product where product.category=?");
query.setString(0, category);
return query.list();
}
});
}
- Usar como base de tus DAO’s la clase HibernateDAOSupport, y dentro de esta usar el HibernateTemplate, o la API de Hibernate directamente:
public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category) throws DataAccessException {
return getHibernateTemplate().find(
"from test.Product product where product.category=?", category);
}
}
public class ProductDaoImpl extends HibernateDaoSupport implements ProductDao {
public Collection loadProductsByCategory(String category)
throws DataAccessException, MyException {
Session session = getSession(getSessionFactory(), false);
try {
List result = session.find(
"from test.Product product where product.category=?",
category, Hibernate.STRING);
if (result == null) {
throw new MyException("invalid search result");
}
return result;
}
catch (HibernateException ex) {
throw convertHibernateAccessException(ex);
}
}
}
- Usar la API de HIbernate 3 para obtener las sesiones:
public class ProductDaoImpl implements ProductDao {
private SessionFactory sessionFactory;
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
public Collection loadProductsByCategory(String category) {
return this.sessionFactory.getCurrentSession()
.createQuery("from test.Product product where product.category=?")
.setParameter(0, category)
.list();
}
}
En los dos primeros casos la ventaja de usarlos en Hibernate 2 es clara: no nos tenemos que ocupar de abrir o cerrar sesiones, lo hace el HibernateTemplate por nosotros, además de hacer que nuestras sesiones entren a formar parte de una transacción (de Spring, de la que luego hablaré).
Claro está que para solucionar el tema del LazyLoading con el que empezé este ladrillo la solución es semejante a la de Hibernate, usar un filtro o un interceptor . Lo bueno es que es el HibernateTemplate el que comprueba si tenemos una sesión asociada al thread o no y cierra la sesión si no existe este filtro (Alguno podría decir que eso también se puede implementar en un HibernateUtil: pues sí, pero con Spring “viene de fábrica” ;-)
En la tercera alternativa el código es el mismo que el que usaríamos en Hibernate (salvo que obtenemos el SessionFactory por IoC), con la salvedad de que ahora podemos usarlo sin necesidad de definir transacciones con JTA, sino con los mecanismos de Spring. Y aquí es donde empiezan las leches entre las divas del bel canto ;-D
DEPRECA LO QUE ME APETECE, INSUSTANCIAL!
Más o menos es lo que parece querer Gavin King respecto a Spring. A raíz de que de la página de hibernate “desaparecieran” las referencias a Spring la polémica estalló, y cómo no, en un blog
Más o menos, Gavin King viene a decir que HibernateTemplate es una bazofia poco elegante y que los admiradores de Hibernate no deberían usarla ya que en Hibernate 3 está tirado obtener sesiones y el manejo de transacciones (con JTA). Y que la “deprequéis” chavalillos de Spring. A lo que los de Spring responden: es mi código y hago lo que quierto con él, sino quieres usar HibernateTemplate NO LO USES, pero no molestes :-D
Vale, pero es una bazofia tan poco elegante como HibernateUtil propuesto en Hibernate 2 (yo diría que menos). Vamos que ha sido una bazofia que ha habido que usar bastante con Hibernate 2.
Por otro lado, se le “olvida” comentar un pequeñísimo detalle, que es la madre del cordero del asunto: con Hibernate si quieres usar getCurrentSession() TIENES que usar transacciones JTA (vamos programadores a pasar por la tabla de EJB3, no os preocupéis de los tiburones que veis ahí abajo!), si usas Spring puedes usarlo usando los mecanismos de transacciones de Spring (que puede usar JTA o no). Osea que si quieres esa comodidad en Hibernate 3 a usar un servidor de aplicaciones (¿te he dicho ya que hibernate pertenece al stack de aplicaciones de Jboss, y que Spring rechazó la oferta de pertenecer a ella?), en cambio Spring te deja la libertad de usarlo dentro o fuera de un servidor de aplicaciones.
Ahora que cada uno tenga su opinión, que yo ya he escrito demasiado ladrillo. En un post siguiente mis problemas con aquellas librerías que usan Hibernate por debajo, y si lo consigo ;-) cómo lo soluciono.
Salu2
Share This
Ultimos comentarios
Offshore: ¡nos fuimos a FICOD! | Blog IBCmass - Consultoría de Tecnologías de la información | Web 2.0 | Diseño Multimedia |, Una presentación chula sobre redes sociales, anassé
alberto, Jesus, alberto, Gozque, gimenete, alberto, gimenete, Félix
Nuestro paso por el FICOD 2008 at Linked, » Por la Conferencia Rails 2008, tog: Proyecto Rails del año 2008 | IBCmass - Consultoría de Tecnologías de la información, gestión de contenidos, usabilidad web y web 2.0, Bettina, ecamacho, alberto, VictorR
Cesar Diaz, Jesus Chuda Contreras, Angeles, Ger, Pedro, Alfonso, Windzor, javier, xelha
Emprendizaje Corporativo e Innovación Abierta « redes de innovación @ ikerlan-ik4, aitor, Miguel, Cashflow | externalidades
FICOD 2008 at Linked