Gérer les sessions Hibernate sans transaction
Par MD3804-GANDI le mercredi 6 octobre 2010, 22:45 - Hibernate - Lien permanent
Comme vous le savez déjà peut être, les sessions Hibernate sont très
fortement couplées aux transactions : Hibernate ouvre et ferme les
sessions au début et à la fin d'une transaction. Lors du développement d'une
librairie (DAO), Je me suis posé la question "est il possible de ne pas
utiliser un contexte transactionnel pour gérer les sessions ?". En effet lors
de mes tests qui sont transactionnels, tout se passait bien mais lorsque
j'intégrais mon code dans un contexte non transactionnel et que je tentais de
récupérer des associations liées à une entité, j'avais des
lazyInitializationException
car les sessions était fermées.
j'avais alors plusieurs solutions :
- Utiliser le filtre Spring
Opensessioninviewfilter
, mais cela rend ma librairie non portable car cela ne fonctionnera que dans un contexte web - Faire des méthodes ad'hoc pour récupérer l'entité ou l'entité avec ses associations chargées (eager), mais au détriment d'un code moins lisible et où il est facile de ne pas utiliser la bonne méthode
- Charger les associations directement (par configuration) avec FetchType=Eager. pas assez performant !
- Utiliser les profiles de fectching
- Faire une gestion des session par thread.
Je vous propose de voir comment j'ai implémenté la dernière solution
Pour ne pas avoir à gérer mes sessions / transactions à la main, j'utilise
Spring, c'est donc à ce dernier que revient la tâche de les ouvrir et de les
fermer. Spring fournit plusieurs "facilities" pour gérer les DAOs, dont celui
d'Hibernate : HibernateDaoSupport
. Cette classe permet de
"wrapper" les méthodes d'Hibernate pouvant faire du CRUD et de s'affranchir de
la gestion des sessions. elle permet également de réalisé un traitement
(callback
) au sein d'une session via une template hibernate
(hibernatTemplate
). Spring ouvre alors une session, exécute le
callback, puis il se chargera de fermé la session :
- Soit à la fin de la transaction
- Soit à la fin de la requête HTTP dans le cas de l'utilisation de
Opensessioninviewfilter
.
Le code d'un callback ressemble alors à cela :
public monDao extends HibernateDaoSupport public Collection loadProductsByCategory(String category) throws DataAccessException { return this.getHibernateTemplate().find( "from test.Product product where product.category=?", category); }
Le déroulement est donc le suivant : HibernateDaoSupport->getHibernatTemplate()->getSession()->Création / réutilisation d'une session selon paramétrage->exécution du callback-> fermeture de la session
il me suffit donc de redéfinir la méthode getsession()
de
HibernateCallback
. Pour cela, il faut que je change l'appel à
getHibernateTemplate()
. Pas de chance! cette méthode est
final
(sick !). je vais devoir rusé. je vais donc remonter
jusqu'au HibernateDaoSupport, et créé le mien :
NotTransationnalHibernateDaoSupport
.
Je reprends donc le code, puis redéfinis la méthode
getHibernateTemplate()
, qui me renverra non plus une
HibernateTemplate
mais une
NonTransactionnalHibernateTemplate
, qui elle même redéfinira
getSession()
, qui elle même s'appuiera sur une politique de
gestion des sessions par Thread.
Pour la gestion par thread je me dis que partir du code de
Opensessioninviewfilter
est un bon départ, car elle permet déjà de
faire une gestion des sessions particulière : il me faut ouvrir une
session, l'affecter dans un ThreadLocal
, et la fermer lorsque le
Thread
disparait
package org.springframework.orm.hibernate3.support; import org.hibernate.FlushMode; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.orm.hibernate3.SessionFactoryUtils; import org.springframework.orm.hibernate3.SessionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; public class OpenSessionInThread extends Thread { public static final String DEFAULT_SESSION_FACTORY_BEAN_NAME = "sessionFactory"; private FlushMode flushMode = FlushMode.MANUAL; private SessionFactory sessionFactory; public OpenSessionInThread(SessionFactory sessionFactory) { super(); this.sessionFactory = sessionFactory; Runtime.getRuntime().addShutdownHook(this); } public Session getSession() { Session session = null; if (TransactionSynchronizationManager.hasResource(sessionFactory)) { session = ((SessionHolder) TransactionSynchronizationManager.getResource(sessionFactory)).getValidatedSession(); } else { session = getSession(sessionFactory); TransactionSynchronizationManager.bindResource(sessionFactory, new SessionHolder(session)); } return session; } protected Session getSession(SessionFactory sessionFactory) throws DataAccessResourceFailureException { Session session = SessionFactoryUtils.getSession(sessionFactory, true); FlushMode flushMode = this.flushMode; if (flushMode != null) { session.setFlushMode(flushMode); } return session; } protected void closeSession() { Session session; if (this.sessionFactory != null) { if (TransactionSynchronizationManager.hasResource(this.sessionFactory)) { session = ((SessionHolder) TransactionSynchronizationManager.getResource(this.sessionFactory)).getValidatedSession(); SessionFactoryUtils.closeSession(session); } else { System.err.println("can not close session, session is not in thread"); } } else { System.err.println("can not close the session because session factory is null"); } } @Override public void run() { closeSession(); } }
Il faut maintenant que j'intègre cela dans la méthode
getSession()
de ma classe
NotTransactionnalHibernateTemplate
:
package org.springframework.orm.hibernate3; import java.sql.SQLException; import org.hibernate.FlushMode; import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; import org.springframework.dao.DataAccessException; import org.springframework.orm.hibernate3.support.OpenSessionInThread; import org.springframework.util.Assert; public class NotTransactionalHibernateTemplate extends HibernateTemplate { private OpenSessionInThread openSessionInThread; public NotTransactionalHibernateTemplate(SessionFactory sessionFactory, boolean allowCreate) { super(sessionFactory, allowCreate); } @Override public boolean isCheckWriteOperations() { return false; } /** * @param sessionFactory */ public NotTransactionalHibernateTemplate(SessionFactory sessionFactory) { super(sessionFactory); openSessionInThread = new OpenSessionInThread(sessionFactory); } @Override public boolean isAllowCreate() { return false; } @Override public boolean isAlwaysUseNewSession() { return false; } @Override protected Session getSession() { Session session = openSessionInThread.getSession(); session.setFlushMode(FlushMode.AUTO); return session; } @Override protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNewSession, boolean enforceNativeSession) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Session session = (enforceNewSession ? SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession()); boolean existingTransaction = (!enforceNewSession && (!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session, getSessionFactory()))); if (existingTransaction) { logger.debug("Found thread-bound Session for HibernateTemplate"); } try { Session sessionToExpose = (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session)); T result = action.doInHibernate(sessionToExpose); session.flush(); return result; } catch (HibernateException ex) { session.clear(); throw convertHibernateAccessException(ex); } catch (SQLException ex) { session.clear(); throw convertJdbcAccessException(ex); } catch (RuntimeException ex) { session.clear(); throw ex; } } }
Ca y est ! maintenant ma gestion des sessions est liée au cycle de vie
du Thread
.
Je redéfinis ensuite la méthode getHibernateTemplate()
de ma
classe NonTransationnalHibernateDaoSupport
:
protected HibernateTemplate createHibernateTemplate(SessionFactory sessionFactory) { return new NotTransactionalHibernateTemplate(sessionFactory); }
maintenant je change l'héritage dans mes DAOs pour qu'il étendent NonTransationnalHibernateDaoSupport.
Il me reste cependant un dernier détail a gérer : il ne faut pas que la
session soit fermée ailleur que dans ma classe de gestion de sessions, or en
regardant où est utilisée getSession()
, je me rend compte que dans
la méthode doExecute()
, la session peut être fermée dans certain
cas. Je modifie donc le code. vous remarquerez que je fais un clear de la
session si une exception est levée afin de ne pas exécuter d'autre
traitement :
@Override protected <T> T doExecute(HibernateCallback<T> action, boolean enforceNewSession, boolean enforceNativeSession) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Session session = (enforceNewSession ? SessionFactoryUtils.getNewSession(getSessionFactory(), getEntityInterceptor()) : getSession()); boolean existingTransaction = (!enforceNewSession && (!isAllowCreate() || SessionFactoryUtils.isSessionTransactional(session, getSessionFactory()))); if (existingTransaction) { logger.debug("Found thread-bound Session for HibernateTemplate"); } try { Session sessionToExpose = (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session)); T result = action.doInHibernate(sessionToExpose); session.flush(); return result; } catch (HibernateException ex) { session.clear(); throw convertHibernateAccessException(ex); } catch (SQLException ex) { session.clear(); throw convertJdbcAccessException(ex); } catch (RuntimeException ex) { session.clear(); throw ex; } }
Afin de testé si cela fonctionne, je désactive les transactions dans mes
tests, et étendAbstractJUnit4SpringContextTests
au lieu de
AbstractTransactionalJUnit4SpringContextTests
, je rajoute
également une méthode pour effacer les enregistrements dans mes tables à la fin
de chaque test :
// public abstract class AbstractDatabaseTestCase extends AbstractTransactionalJUnit4SpringContextTests{ public abstract class AbstractDatabaseTestCase extends AbstractJUnit4SpringContextTests{ @After public void cleanAll() { dao1.flushAndClear(); dao1.deleteAll(dao1.getAll()); dao2.flushAndClear(); dao2.deleteAll(dao2.getAll()); .... }
Reste à vérifier si cela fonctionne !
En l'absence de transaction, si mes DAO étendent
NonTransationnalHibernateDaoSupport
, les associations chargées en
lazy ne font plus de lazyInitializationException
alors que c'est
le cas si mes DAOs étendent HibernateDaoSupport
CQFD !
pour ceux qui sont intéressé, j'ai joint les sources à ce billet.
Commentaires
J'ai développé mon site web en java/j2ee et hibernate pour la persistance.
j'ai effectivement de gros problèmes avec cela : une annonce sur mon site web est lié à un compte, le profil du compte, à une catégorie, des évaluations, des zones d'interverventions, etc....
pour ne charger que ce qui m'intéresse, j'ai pour le moment des méthodes ad hoc mais cela n'est vraiment pas simple comme tu le dis très justement !
j'ai lu ton article en diagonal (jsuis au boulot :p) mais je vais essayer ta solution très rapidement et faire un feedback ici :)
à bientôt et encore merci !!
Bonjour David, j'ai essayé de mettre en place votre gestion mais je n'y arrive pas.
Je me retrouve avec cette erreur :
Exception in thread "main" java.lang.IllegalStateException: No Hibernate Session bound to thread, and configuration does not allow creation of non-transactional one here.
....
....
can not close session, session is not in thread
Pouvez-vous fournir le fichier spring svp cela pourrait m'aider?
Moi j'ai cela :
<bean id="hibernateTemplate" class="org.springframework.orm.hibernate3.NotTransactionalHibernateTemplate">
<constructor-arg index="0" ref="sessionFactory" />
</bean>
<!-- Hibernate Transaction Manager Definition -->
<bean id="transactionManager" class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory">
<ref local="sessionFactory" />
</property>
</bean>
<!-- <tx:annotation-driven transaction-manager="transactionManager" />-->
<!-- Déclaration du Dao Parent, dont héritent tous les autres DAOs -->
<bean id="IDataAccessObject" abstract="true" class="fr.coursescompare.framework.persistance.DataAccessObject">
<property name="hibernateTemplate" ref="hibernateTemplate" />
</bean>
avec <prop key="current_session_context_class">thread</prop> dans la sessionFactory.
MERCI d'avances pour votre aide.
Bonjour,
pas besoin de redefinir hibernateTemplate, dans spring, ni current_session_context_class (le but etant de ne rien changer). par contre laisser le tx manager.meme s'il ne sert pas il est necessaire
il suffit juste que vos dao etendent NonTransationnalHibernateDaoSupport. ca doit etre le seul changement. c'est pour cela que vous avez le message "configuration does not allow creation of non-transactional one here."
dans le cas Spring=>
sessionfactory.getCurrentSession=>currentSessionContext.currentSession()=>SpringSessionContext=>SessionFactoryUtils.doGetSession=>TransactionSynchronizationManager
si hibernate.current_session_context_class=thread, SpringSessionContext n'est pas utilisé mais ThreadLocalSessionContext, regarder les source vous aidera sans doute à voir plus clair
David