Transactions avec Hibernate et Spring

L'architecture classique d'une application java ou java EE est organisée en trois couches :

Dans une implémentation propre de cette architecture, ces couches sont strictement disjointes. Une des clés de ce type d'architecture est la gestion des transactions : elle doit être implémentée au niveau services, sans pour autant tenir compte de la technologie utilisée au sein de la couche DAO (Hibernate, JPA, JDBC,...).


Architecture-spring.png

Ce "Tip" présente les façons traditionnelles de gérer les transactions avec Spring et Hibernate, mais aussi un technique alternative, adaptée au projets en cours d'intégration de Spring. Le cas des transactions JTA est laissé de coté. Il a été testé avec Spring 2.5, JUnit 4.4 et Hibernate 3.2, sous Eclipse 3.3.

Transactions Hibernate

Le démarrage et la manipulation des transactions se fait par l'intermédiaire de l'objet de session Hibernate. Plusieurs requêtes peuvent donc participer à une même transaction si elles utilisent la même session.

Partage de session

Pour utiliser un session partagée, les classes de DAO doivent utiliser le même objet de SessionFactory, en le mettant par exemple en champs statique d'une classe utilitaire.

 public class HibernateUtil {
   private static SessionFactory sessionFactory;
   
   public static SessionFactory getSessionFactory() {
     if (sessionFactory == null)
       throw new IllegalStateException("Le sessionFactory n'a pas été initialisé.");
     return sessionFactory;
   }
   
   public static void initialize() {
     Configuration config = new Configuration();
     ...
     sessionFactory = config.buildSessionFactory();
   }
 }

La session est ensuite récupérée par la méthode getCurrentSession.

   Session currentSession = HibernateUtil.getSessionFactory().getCurrentSession();

Dans ce fonctionnement, Hibernate cherche à stocker la session dans le thread courant. Pour permettre ce fonctionnement, la configuration (fichier hibernate.cfg.xml) doit inclure la propriété suivante :

 <hibernate-configuration>
   ...
   <property name="hibernate.transaction.factory_class">
     org.hibernate.transaction.JDBCTransactionFactory
   </property>
   <property name="hibernate.current_session_context_class">
     thread
   </property>
 </hibernate-configuration>

Remarque : la propriété hibernate.transaction.factory_class n'est pas obligatoire, car nous l'avons renseignée ici avec sa valeur par défaut.

Démarrage et validation de transaction

L'objet de session fournit des méthodes pour démarrer une transaction et pour récupérer une transaction en cours :

   // Démarrer une nouvelle transaction
   Transaction transaction = currentSession.beginTransaction()
   
   // Récupérer la transaction courante
   Transaction transaction = currentSession.getTransaction()

La validation ou l'annulation de la transaction se fait sur l'objet Transaction.

   // Validation de la transaction
   transaction.commit();
   
   // Annulation de la transaction
   transaction.rollback();

Dans ce mode de fonctionnement, la validation de la transaction (commit) déclenche le flush et ferme la session courante. La portion de code suivante provoquera donc une exception :

   // Récupérer la session courante
   Session currentSession = HibernateUtil.getSessionFactory().getCurrentSession();
   // Récupérer la transaction courante
   Transaction transaction = currentSession.getTransaction()
   // Validation de la transaction
   transaction.commit();
   // Clôture de la session
   session.close();

Cette portion de code déclenche une exception org.hibernate.SessionException avec le message "Session was already closed".

Intégration dans l'architecture

Dans la technique présentée ici, on constate que toute la gestion des transactions passe par la manipulation de classes spécifiques à Hibernate. Hors ceci est en contradiction avec le principe d'isolation des couches : la couche de service ne devrait pas connaître la technique de persistance utilisée par la couche DAO.

Il est donc nécessaire d'ajouter dans notre architecture des couches d'abstraction pour mieux gérer les transactions. Ces couches peuvent être fournis par les fonctionnalités d'inversion de contrôle (ou IoC) et d'aspects (AOP) de certains frameworks classiques comme Spring.


Intégration Spring / Hibernate

Dans le schéma présenté en introduction, les composants de DAO sont intégrés à Spring, ce qui peut faciliter la gestion de la session et des transactions.

Bean de SessionFactory

La façon traditionnelle d'intégrer des services Hibernate dans Spring est de déclarer un bean de type SessionFactory, par lequel passera toute création de session Hibernate. Ce bean remplace totalement le fichier hibernate.cfg.xml.

 <bean id="sessionFactory"
   class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
   <property name="dataSource" ref="dataSource" />
   <property name="hibernateProperties">
     <props>
       <prop key="hibernate.dialect">
         org.hibernate.dialect.HSQLDialect
       </prop>
     </props>
   </property>
   <property name="annotatedClasses">
     <list>
       <value>fr.sewatech.data.university.CourseData</value>
     </list>
   </property>
 </bean>

Bean de TransactionManager

Pour que Spring puisse gérer des transactions Hibernate, on ajoute un bean spécialisé dans cette tâche.

   <bean id="txManager"
         class="org.springframework.orm.hibernate3.HibernateTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
   </bean>

Transactions par programmation

En accédant au TransactionManager, il est possible de démarrer et de conclure des transactions depuis la classe de test. Ces transactions engloberons naturellement les requêtes soumises à la base par le composant DAO.

 @Service("courseService")
 public class CourseServiceImpl implements CourseService {      
   @Autowired // Injection  du transactionManager
   private PlatformTransactionManager transactionManager;
   
   public void justeFaisLe(CourseData course) {
     // Ouverture de la transaction, avec les paramètres par défaut (PROPAGATION_REQUIRED)
     TransactionStatus status = transactionManager.getTransaction(null);
     try {
       ...
       // Validation de la transaction
       transactionManager.commit(status);
     } catch (Exception e) {
       transactionManager.rollback(status);
     }
   }
   ...
 }

Transactions par AOP

C'est probablement ma technique préférée, car elle ne nécessite aucune modification dans le code source. Son principal défaut est qu'elle se base sur des conventions de développement relativement fortes, sur le nommage des classes, packages et méthodes.

La partie <aop:config> paramètre les classes qui seront prises en compte et la partie <tx:advice> précise la statégie de transaction en fonction des méthodes.

 <beans ...>
   <bean id="txManager"
         class="org.springframework.orm.hibernate3.HibernateTransactionManager">
     <property name="sessionFactory" ref="sessionFactory" />
   </bean>
    
   <tx:advice id="serviceTxAdvice" transaction-manager="txManager">
     <tx:attributes>
       <tx:method name="find*" propagation="REQUIRED" read-only="true" />
       <tx:method name="*" propagation="REQUIRED" />
     </tx:attributes>
   </tx:advice>
   
   <aop:config>
     <aop:pointcut id="serviceMethods"
         expression="execution(* fr.sewatech.university.service.*Service.*(..))" />
     <aop:advisor advice-ref="serviceTxAdvice" pointcut-ref="serviceMethods" />
   </aop:config>
 </beans>
   

Remarque : dans la partie <aop:config>, l'expression peut désigner les classes ou les interfaces des services.

Transactions par annotation

Depuis Spring 2.0, avec la JVM 5, des annotations de transaction sont disponibles. Un article traite déjà des annotations avec Spring 2.0 et Spring 2.5, et en particulier des annotations de transaction.

Techniques alternatives

Dans certains projets, les techniques traditionnelles ne peuvent pas être implémentées facilement à cause d'un historique sans Spring et d'erreurs de programmation antérieures.

Configuration hibernate.cfg.xml

Dans l'exemple de configuration précédente, le bean de SessionFactory remplace totalement le fichier de configuration hibernate.cfg.xml. Dans le cadre d'une intégration progressive de Spring, il peut être intéressant de brancher le SessionFactory sur un fichier hibernate.cfg.xml existant :

<bean id="sessionFactory" class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean"> <property name="configLocation"> <value>hibernate.cfg.xml</value> </property> </bean>

Le problème de cette technique est que la gestion automatique des transactions par Spring ne fonctionne plus. Un contournement est de modifier le fichier de configuration d'Hibernate :

 <hibernate-configuration>
 	<session-factory>
     ...
 		<property name="hibernate.current_session_context_class">
 			org.springframework.orm.hibernate3.SpringSessionContext
 		</property>
     ...
 	</session-factory>
 </hibernate-configuration>

Un nouveau problème apparaît alors : Les sessions et transactions ne fonctionnent plus hors de Spring ! Ce qui est très gênant pour une intégration progressive.

Pour contourner ce nouveau problème, il faut initialiser la synchronisation entre Spring et les threads :

 if (! TransactionSynchronizationManager.isSynchronizationActive())
   TransactionSynchronizationManager.initSynchronization();

L'initialisation doit être faite une seule fois par thread, sous peine d'une exception de type java.lang.IllegalStateException, avec le message "Cannot activate transaction synchronization - already active". C'est pour éviter celà que je fais un test préalable.

Grâce à cette technique, les mêmes classes de DAO peuvent être utilisées par des services dans Spring, utilisant donc le TransactionManager de Spring, et par des services externes à Spring, manipulant directement des sessions et transactions via les classes d'Hibernate.

Attention : cette technique n'est généralement pas préconisée ; elle est utile dans une période de transition.