home : Développement : Base de données :
Gestion des connexions non-transactionnelles #3
Lundi, 28 Mars 2011 00:00 PDF Imprimer Envoyer

Dans un deuxième article, nous avions configuré et étudié le comportement de la solution standard. Nous avions notamment constaté que, pour les méthodes en écriture comme pour les méthodes en lecture seule, une connexion était ouverte et réquisitionnée tout le long de l'appel.

Dans ce nouvel article, nous allons proposer une nouvelle solution permettant de libérer la connexion à la base de données lorsqu'elle n'est pas nécessaire lors d'un appel en lecture seule, tout en conservant le contexte transactionnel pour les méthodes d'écriture.

Présentation théorique de la solution

Pour les méthodes en écriture, nous allons conserver la solution proposée dans l'article précédent. En revanche, pour les méthodes en lecture seule, nous allons modifier un peu le comportement de Spring et d'Hibernate en n'ouvrant pas une connexion au niveau de la couche Façade, mais en provoquant l'ouverture uniquement lorsque cela est nécessaire. Voici le diagramme de séquence revu et corrigé :

alt

  1. Hibernate souhaite accéder à la base de données, nous ouvrons puis fermons la connexion,
  2. Idem,
  3. L'appel aux Web Services pourra être un peu long, la connexion ne sera pas réquisitionnée.

Le principe est donc de n'avoir une ouverture que lorsque Hibernate souhaite accéder à la base de données. Pour cela, au lieu d'ouvrir un contexte transactionnel complet sur la couche Façade, nous n'allons ouvrir qu'une session Hibernate seule, sans transaction ni connexion ouverte.

Configuration de Spring

Naturellement, la configuration de Spring commence à s'étoffer un peu, en comparaison de la configuration, très simple, de la solution précédente :

 
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
 
   <import resource="applicationContext-datasource.xml"></import>
   <import resource="applicationContext-facades.xml"></import>
 
 
   <bean class="org.springframework.orm.hibernate3.LocalSessionFactoryBean" id="sessionFactory" scope="singleton">
      <property name="configLocation" value="classpath:hibernate/hibernate-read-only.cfg.xml"></property>
      <property name="dataSource" ref="datasource"></property>
   </bean>
 
   <bean class="org.springframework.orm.hibernate3.HibernateTransactionManager" id="transactionManager">
      <property name="sessionFactory" ref="sessionFactory"></property>
   </bean>
 
   <bean class="org.springframework.orm.hibernate3.HibernateInterceptor" id="sessionOpeningInterceptor">
      <property name="sessionFactory" ref="sessionFactory"></property>
   </bean>
 
 
 
   <bean class="org.springframework.transaction.interceptor.TransactionInterceptor" id="transactionOpeningInterceptor">
      <property name="transactionManager" ref="transactionManager"></property>
      <property name="transactionAttributeSource">
         <value>
            com.thug.facade.CustomerFacade.*=PROPAGATION_REQUIRES_NEW,-Throwable
         </value>
      </property>
   </bean>
 
   <bean class="com.thug.tools.CustomRegexpMethodPointcutAdvisor" id="transactionOpeningInterceptorPointCut">
      <property name="advice" ref="transactionOpeningInterceptor"></property>
      <property name="patterns">
         <list>
            <value>com.thug.facade.CustomerFacade.*</value>
         </list>
      </property>
      <property name="excludedPatterns">
         <list>
            <value>com.thug.facade.CustomerFacade.(get).*$</value>
         </list>
      </property>
   </bean>
 
   <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" id="sessionOpeningPointCut">
      <property name="advice" ref="sessionOpeningInterceptor"></property>
      <property name="patterns">
         <list>
            <value>com.thug.facade.CustomerFacade.(get).*$</value>
         </list>
      </property>
   </bean>
 
   <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator" id="transactionBeanNameProxyCreator">
      <property name="proxyTargetClass" value="true">
      <property name="beanNames">
         <list>
            <value>customerFacade</value>
         </list>
      </property>
      <property name="interceptorNames">
         <list>
            <value>transactionOpeningInterceptorPointCut</value>
            <value>sessionOpeningPointCut</value>
         </list>
      </property>
   </property></bean>
 
</beans>
 

Si l'on commente un peu ce qui se trouve dans ce fichier Spring :

  • On trouve les mêmes SessionFactory et TransactionManager que dans la configuration précédente,
  • Un nouveau bean SessionOpeningInterceptor a vu le jour. Il permettra l'ouverture de la session Hibernate sur la couche Façade, sans liaison directe avec une connexion. Notez cependant qu'il a accès à la SessionFactory, et donc à la datasource,
  • Le reste de la configuration est relativement classique et facilement compréhensible : on branche le TransactionManager sur toutes les méthodes de couche Façade, en excluant les méthodes de lecture seule. Notez que nous n'avons ici qu'une seule façade, mais qu'il pourrait facilement y en avoir plus. Notez également que le pattern de sélection des méthodes est configurable et pourrait contenir "(get|is|find)" afin de cibler plus facilement un ensemble de méthodes. De la même façon, on branche le SessionOpeningInterceptor sur les méthodes de lecture.

La classe "outil" d'exclusion

Si vous souhaitez pouvoir exclure facilement des méthodes (au lieu de configurer toutes les méthodes à inclure), vous pouvez utiliser cette petite classe "outil", utilisée dans la configuration Spring précédente :

 
import org.springframework.aop.support.AbstractRegexpMethodPointcut;
import org.springframework.aop.support.JdkRegexpMethodPointcut;
import org.springframework.aop.support.RegexpMethodPointcutAdvisor;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
 
public class CustomRegexpMethodPointcutAdvisor extends RegexpMethodPointcutAdvisor {
 
   private static final long serialVersionUID = 1L;
 
   private S tring[] excludedPatterns = new S tring[0];
 
   @Override
   protected AbstractRegexpMethodPointcut createPointcut() {
 
      AbstractRegexpMethodPointcut pointcut = new JdkRegexpMethodPointcut();
      if (this.excludedPatterns != null && this.excludedPatterns.length > 0) {
         pointcut.setExcludedPatterns(this.excludedPatterns);
      }
      return pointcut;
   }
 
   public void setExcludedPattern(S tring excludedPattern) {
      setExcludedPatterns(new S tring[] { excludedPattern });
   }
 
   public void setExcludedPatterns(S tring[] excludedPatterns) {
 
      this.excludedPatterns = new S tring[excludedPatterns.length];
      for (int i = 0; i < excludedPatterns.length; i++) {
         this.excludedPatterns[i] = StringUtils.trimWhitespace(excludedPatterns[i]);
      }
   }
 
   public S tring[] getExcludedPatterns() {
      return this.excludedPatterns;
   }
}
 

Configuration d'Hibernate

Si nous nous arrêtons ici dans la configuration du projet, nous aurons le comportement suivant :

  1. Ouverture d'une session Hibernate sur la couche Façade,
  2. Ouverture d'une connexion dès que Hibernate en a besoin,
  3. Fermeture de la connexion en même temps que la fermeture de la session Hibernate, c'est-à-dire sur la façade.

Ce dernier point n'est pas pour nous arranger, puisque nous souhaitons fermer la connexion dès que Hibernate a effectué sa requête. Pour cela, nous allons ajouter une petite classe de notre cru nous permettant de fermer la connexion quand nous le désirons. Commençons par modifier le fichier de configuration d'Hibernate en ajoutant la ligne suivante :

 
<event type="post-load">
   <listener class="com.thug.tools.ConnectionCloserPostLoadEvenListener"></listener>
</event>
 

Cette configuration a pour effet d'appeler notre classe spécifique chaque fois qu'un chargement se termine. Voici le code correspondant de la classe :

 
public class ConnectionCloserPostLoadEvenListener extends DefaultPostLoadEventListener implements PostLoadEventListener {
 
   private static final long serialVersionUID = 1L;
 
   public void onPostLoad(PostLoadEvent event) {
 
      super.onPostLoad(event);
 
      // if a connection, which does not take part into a transaction, is opened, we close it
      if (!event.getSession().getJDBCContext().isTransactionInProgress()) {
         event.getSession().getJDBCContext().getConnectionManager().manualDisconnect();
      }
   }
}

Tout en conservant le comportement standard d'Hibernate (cf. l'appel au super.onPostLoad), nous fermons la connexion ouverture si et seulement si elle ne fait pas partie d'un contexte transactionnel : ainsi dans le cas des appels de méthodes en lecture seule, nous fermons la connexion ; mais dans le cas des méthodes en écriture, nous ne faisons rien, de façon à ne pas troubler la transaction.

Résultat sur la méthode getCustomer

Etudions maintenant les logs de la méthode getCustomer, afin de pouvoir comparer le résultat avec la solution précédente :

 
09:59:56.419 [INFO ] com.thug.AbstractTestCase - First call to facade.
09:59:56.446 [INFO ] com.thug.facade.CustomerFacade - start - getCustomer

Jusqu'ici, aucune connexion n'a été ouverte. Nous sommes pourtant passés à travers les couches Façade et Service. Nous arrivons maintenant dans la couche DAO :

 
09:59:56.446 [INFO ] com.thug.dao.CustomerDao - get customer
09:59:56.449 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection
Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=?
09:59:56.462 [INFO ] com.thug.tools.MyConnection - Closing connection.

L'appel à la méthode DAO commence avec l'ouverture d'une connexion. L'exécution de la requête standard s'effectue dans les conditions classiques, mais est directement suivie par la fermeture de la connexion ouverte. Ainsi, pendant l'appel aux Web Services suivant, la connexion est libérée :

 
09:59:56.463 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services...
09:59:56.463 [INFO ] com.thug.client.ProviderWs - Web Services are slow.
10:00:01.627 [INFO ] com.thug.facade.CustomerFacade - Web Services called.

Nous sommes ici revenus à la couche Façade où nous devons faire appel à nos adresses lazy-loadées. Là encore, une connexion est ouverte puis fermée automatiquement, avant la fin de l'appel façade.

 
10:00:01.628 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses...
10:00:01.628 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection
Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=?
10:00:01.632 [INFO ] com.thug.tools.MyConnection - Closing connection.
10:00:01.632 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got.
10:00:01.632 [INFO ] com.thug.facade.CustomerFacade - end (5186ms)

Mission accomplie.

Le deuxième appel se déroule exactement de la même façon :

 
10:00:01.644 [INFO ] com.thug.AbstractTestCase - Second call to facade.
10:00:01.645 [INFO ] com.thug.facade.CustomerFacade - start - getCustomer
10:00:01.645 [INFO ] com.thug.dao.CustomerDao - get customer
10:00:01.645 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection
Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=?
10:00:01.646 [INFO ] com.thug.tools.MyConnection - Closing connection.
10:00:01.646 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services...
10:00:01.646 [INFO ] com.thug.client.ProviderWs - Web Services are slow.
10:00:06.646 [INFO ] com.thug.facade.CustomerFacade - Web Services called.
10:00:06.647 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses...
10:00:06.647 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection
Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=?
10:00:06.649 [INFO ] com.thug.tools.MyConnection - Closing connection.
10:00:06.650 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got.
10:00:06.650 [INFO ] com.thug.facade.CustomerFacade - end (5005ms)

Résultat sur la méthode updateCustomer

En revanche, l'appel à la méthode updateCustomer, en écriture, ne change pas de la solution précédente : l'ouverture et la fermeture de la connexion se font avant et après l'accès à la façade. Nous conservons ainsi notre contexte transactionnel, comme nous le souhaitions. 

 
10:00:06.738 [INFO ] com.thug.tools.MyBasicDataSource - Opening connection
10:00:06.740 [INFO ] com.thug.facade.CustomerFacade - start - updateCustomerName
10:00:06.740 [INFO ] com.thug.dao.CustomerDao - get customer
Hibernate: select customer0_.id_customer as id1_0_0_, customer0_.first_name as first2_0_0_, customer0_.last_name as last3_0_0_ from customer customer0_ where customer0_.id_customer=?
10:00:06.740 [INFO ] com.thug.facade.CustomerFacade - Calling Web Services...
10:00:06.740 [INFO ] com.thug.client.ProviderWs - Web Services are slow.
10:00:11.742 [INFO ] com.thug.facade.CustomerFacade - Web Services called.
10:00:11.742 [INFO ] com.thug.facade.CustomerFacade - Getting lazy addresses...
Hibernate: select addresses0_.id_customer as id4_1_, addresses0_.id_address as id1_1_, addresses0_.id_address as id1_1_0_, addresses0_.city as city1_0_, addresses0_.country as country1_0_ from address addresses0_ where addresses0_.id_customer=?
10:00:11.744 [INFO ] com.thug.facade.CustomerFacade - Lazy addresses got.
10:00:11.744 [INFO ] com.thug.facade.CustomerFacade - Setting new last name...
10:00:11.745 [INFO ] com.thug.facade.CustomerFacade - Last name set.
10:00:11.745 [INFO ] com.thug.facade.CustomerFacade - Updating customer...
10:00:11.745 [INFO ] com.thug.dao.CustomerDao - update customer
10:00:11.747 [INFO ] com.thug.facade.CustomerFacade - Customer updated.
10:00:11.747 [INFO ] com.thug.facade.CustomerFacade - end (5007ms)
Hibernate: update customer set first_name=?, last_name=? where id_customer=?
10:00:11.766 [INFO ] com.thug.tools.MyConnection - Closing connection.

(Avantages et) inconvénients

Nous n'allons pas revenir sur les avantages de libérer la connexion pour les méthodes en lecture seule (cf. les articles précédents). En revanche, nous allons étudier les inconvénients de cette solution.

Tout d'abord, elle modifie le comportement d'Hibernate, à travers la surcharge de l'évènement post load. Bien que le comportement soit celui attendu et que ce dernier soit fonctionnel, nous entrons ici dans les arcanes du framework, rendant la solution un peu moins élégante.

Il faut également prendre conscience que rien ne garantit qu'un appel à une méthode en lecture seule, libérant une connexion, soit capable d'en rouvrir une autre par la suite pour terminer le process. Si votre pool de connexion est vide, il va en résulter une attente de connexion libre, attente qui va dégrader les performances du temps d'appel, voire le faire échouer complètement si elle dépasse la limite maximale autorisée. Il faut cependant relativiser : si nous mettons en place cette solution, c'est justement pour soulager le pool de connexion. Une configuration adaptée de ce dernier devrait limiter l'apparition de cet effet secondaire.

Un autre effet négatif est l'ouverture et la fermeture systématiques de connexions. Si votre process est mal conçu ou que vous abusez de cette solution (voire du lazy-loading), votre application va passer son temps à ouvrir et fermer des connexions, ce qui peut éventuellement ne pas plaire à votre DBA. Cependant, là encore des solutions existent : le système de cache permet déjà de limiter l'effet yoyo (cf. ci-dessous). Vous avez également la possibilité de placer une partie de votre process, dans lequel est groupé un ensemble de chargements de données, derrière une transaction temporaire (par exemple, une conversion d'un objet BO vers son homologue de la couche façade pourrait être spécialement localisée derrière une façade de conversion).

A vous de mixer la solution proposée avec ce que vous faites déjà actuellement, en fonction de votre process métier.

Amélioration de la solution

Nous avons déjà rempli une bonne partie du contrat que nous nous étions fixé au début de cette série d'articles : libérer les connexions ouvertes inutilement. Cependant, la solution n'est pas encore parfaite : le second appel ouvre encore des connexions, alors que les données chargées sont les mêmes que lors du premier. Nous pouvons encore économiser deux ouvertures/fermetures avec la mise en place d'un cache.

Dans le prochain article, nous allons mettre en place un cache de niveau 2 et constater les effets de ce dernier sur le comportement du second appel.

Mise à jour le Jeudi, 31 Mars 2011 07:22 Thomas Huguerre
 

Ajouter un Commentaire


Code de sécurité
Rafraîchir