Gérer les sessions Hibernate sans transaction

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

1. Le lundi 11 octobre 2010, 10:50 par toupil

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 !!

2. Le mardi 16 novembre 2010, 00:07 par nairbeau

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.

3. Le mercredi 17 novembre 2010, 20:20 par david

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

La discussion continue ailleurs

URL de rétrolien : https://davidmasclet.gisgraphy.com/index.php?trackback/10

Fil des commentaires de ce billet