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.