Chapitre 14. Améliorer les performances

14.1. Comprendre les performances des Collections

Nous avons déjà passé du temps à discuter des collections. Dans cette section, nous allons traiter du comportement des collections à l'exécution.

14.1.1. Classification

Hibernate définit trois types de collections :

  • les collections de valeurs

  • les associations un-vers-plusieurs

  • les associations plusieurs-vers-plusieurs

Cette classification distingue les différentes relations entre les tables et les clés étrangères mais ne nous apprend rien de ce que nous devons savoir sur le modèle relationnel. Pour comprendre parfaitement la structure relationnelle et les caractéristiques des performances, nous devons considérer la structure de la clé primaire qui est utilisée par Hibernate pour mettre à jour ou supprimer les éléments des collections. Celà nous amène aux classifications suivantes :

  • collections indexées

  • sets

  • bags

Toutes les collections indexées (maps, lists, arrays) ont une clé primaire constituée des colonnes clé (<key>) et <index>. Avec ce type de clé primaire, la mise à jour de collection est en général très performante - la clé primaire peut être indexées efficacement et un élément particulier peut être localisé efficacement lorsqu'Hibernate essaie de le mettre à jour ou de le supprimer.

Les Sets ont une clé primaire composée de <key> et des colonnes représentant l'élément. Elle est donc moins efficace pour certains types de collections d'éléments, en particulier les éléments composites, les textes volumineux ou les champs binaires ; la base de données peut ne pas être capable d'indexer aussi efficacement une clé primaire aussi complexe. Cependant, pour les associations un-vers-plusieurs ou plusieurs-vers-plusieurs, spécialement lorsque l'on utilise des entités ayant des identifiants techniques, il est probable que cela soit aussi efficace (note : si vous voulez que SchemaExport créé effectivement la clé primaire d'un <set> pour vous, vous devez déclarer toutes les colonnes avec not-null="true").

Le pire cas intervient pour les Bags. Dans la mesure où un bag permet la duplications des éléments et n'a pas de colonne d'index, aucune clé primaire ne peut être définie. Hibernate n'a aucun moyen de distinguer des enregistrements dupliqués. Hibernate résout ce problème en supprimant complètement les enregistrements (via un simple DELETE), puis en recréant la collection chaque fois qu'elle change. Ce qui peut être très inefficace.

Notez que pour une relation un-vers-plusieurs, la "clé primaire" peut ne pas être la clé primaire de la table en base de données - mais même dans ce cas, la classification ci-dessus reste utile (Elle explique comment Hibernate "localise" chaque enregistrement de la collection).

14.1.2. Les lists, les maps et les sets sont les collections les plus efficaces pour la mise à jour

La discussion précédente montre clairement que les collections indexées et (la plupart du temps) les sets, permettent de réaliser le plus efficacement les opérations d'ajout, de suppression ou de modification d'éléments.

Il existe un autre avantage qu'ont les collections indexées sur les Sets dans le cadre d'une association plusieurs vers plusieurs ou d'une collection de valeurs. A cause de la structure inhérente d'un Set, Hibernate n'effectue jamais d'UPDATE quand un enregistrement est modifié. Les modifications apportées à un Set se font via un INSERT et DELETE (de chaque enregistrement). Une fois de plus, ce cas ne s'applique pas aux associations un vers plusieurs.

Après s'être rappelé que les tableaux ne peuvent pas être chargés tardivement, nous pouvons conclure que les lists, les maps et les sets sont les types de collections les plus performants. (tout en remarquant, que pour certaines valeurs de collections, les sets peuvent être moins performants).

Les sets sont considérés comme le type de collection le plus répendu dans des applications basées sur Hibernate.

Il existe une fonctionnalité non documentée dans cette version d'Hibernate : les mapping <idbag> implémentent la sémantique des bags pour une collection de valeurs ou une association plusieurs vers plusieurs et sont plus performants que les autres types de collections dans le cas qui nous occupe !

14.1.3. Les Bags et les lists sont les plus efficaces pour les collections inverse

Avant que vous n'oubliez les bags pour toujours, il y a un cas précis où les bags (et les lists) sont bien plus performants que les sets. Pour une collection marquée comme inverse="true" (le choix le plus courant pour un relation un vers plusieurs bidirectionnelle), nous pouvons ajouter des éléments à un bag ou une list sans avoir besoin de l'initialiser (fetch) les éléments du sac! Ceci parce que Collection.add() ou Collection.addAll() doit toujours retourner vrai pour un bag ou une List (contrairement au Set). Cela peut rendre le code suivant beaucoup plus rapide.

Parent p = (Parent) sess.load(Parent.class, id);
    Child c = new Child();
    c.setParent(p);
    p.getChildren().add(c);  //pas besoin de charger la collection !
    sess.flush();

14.1.4. Suppression en un coup

Parfois, effacer les éléments d'une collection un par un peut être extrêmement inefficace. Hibernate n'est pas totalement stupide, il sait qu'il ne faut pas le faire dans le cas d'une collection complètement vidée (lorsque vous appellez list.clear(), par exemple). Dans ce cas, Hibernate fera un simple DELETE et le travail est fait !

Supposons que nous ajoutions un élément dans une collection de taille vingt et que nous enlevions ensuite deux éléments. Hibernate effectuera un INSERT puis deux DELETE (à moins que la collection ne soit un bag). Ce qui est souhaitable.

Cependant, supposons que nous enlevions dix huit éléments, laissant ainsi deux éléments, puis que nous ajoutions trois nouveaux éléments. Il y a deux moyens de procéder.

  • effacer dix huit enregistrements un à un puis en insérer trois

  • effacer la totalité de la collection (en un DELETE SQL) puis insérer les cinq éléments restant un à un

Hibernate n'est pas assez intelligent pour savoir que, dans ce cas, la seconde méthode est plus rapide (Il plutôt heureux qu'Hibernate ne soit pas trop intelligent ; un tel comportement pourrait rendre l'utilisation de triggers de bases de données plutôt aléatoire, etc...).

Heureusement, vous pouvez forcer ce comportement lorsque vous le souhaitez, en liberant (c'est-à-dire en déréférençant) la collection initiale et en retournant une collection nouvellement instanciée avec les éléments restants. Ceci peut être très pratique et très puissant de temps en temps.

Nous avons déjà présenté l'utilisation de l'initialisation tardive pour les collections persistantes dans le chapitre sur le mapping des collections. Une fonctionnalité similaire existe pour les références aux objets ordinaires, elle utilise les proxys CGLIB. Nous avons également mentionné comment Hibernate met en cache les objets persistants au niveau de la Session. Des stratégies de cache plus aggressives peuvent être configurées classe par classe.

Dans la section suivante, nous vous montrerons comment utiliser ces fonctionnalités, qui peuvent être utlisées pour atteindre des performantes plus élevées, quand cela est nécessaire.

14.2. Proxy pour une Initialisation Tardive

Hibernate implémente l'initialisation tardive d'objets persistants via la génération de proxy par bytecode enhancement à l'exécution (grâce à l'excellente bibliothèque CGLIB).

Le fichier de mapping déclare une classe ou une interface à utiliser comme interface proxy pour la classe. L'approche recommandée est de définir la classe elle-même :

<class name="eg.Order" proxy="eg.Order">

Le type des proxys à l'exécution sera une sous classe de Order. Notez que les classes soumises à proxy doivent implémenter un contructeur par défaut avec au minimum la visibilité package.

Il y a quelques précautions à prendre lorsque l'on étend cette approche à des classes polymorphiques, exemple :

<class name="eg.Cat" proxy="eg.Cat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.DomesticCat">
        .....
    </subclass>
</class>

Tout d'abord, les instances de Cat ne pourront jamais être "castées" en DomesticCat, même si l'instance sous jacente est une instance de DomesticCat.

Cat cat = (Cat) session.load(Cat.class, id);  // instancie un proxy (n'interroge pas la base de données)
if ( cat.isDomesticCat() ) {                  // interroge la base de données pour initialiser le proxy
    DomesticCat dc = (DomesticCat) cat;       // Erreur !
    ....
}

Deuxièmement, il est possible de casser la notion d'== des proxy.

Cat cat = (Cat) session.load(Cat.class, id);            // instancie un proxy Cat
DomesticCat dc = 
    (DomesticCat) session.load(DomesticCat.class, id);  // un nouveau proxy Cat est requis !
System.out.println(cat==dc);                            // faux

Cette situation n'est pas si mauvaise qu'il n'y parait. Même si nous avons deux références à deux objets proxys différents, l'instance de base sera quand même le même objet :

cat.setWeight(11.0);  // interroge la base de données pour initialiser le proxy
System.out.println( dc.getWeight() );  // 11.0

Troisièmement, vous ne pourrez pas utiliser un proxy CGLIB pour une classe final ou pour une classe contenant la moindre méthode final.

Enfin, si votre objet persistant obtient une ressource à l'instanciation (par example dans les initialiseurs ou dans le contructeur par défaut), alors ces ressources seront aussi obtenues par le proxy. La classe proxy est vraiment une sous classe de la classe persistante.

Ces problèmes sont tous dus aux limitations fondamentales du modèle d'héritage unique de Java. Si vous souhaitez éviter ces problèmes, vos classes persistantes doivent chacune implémenter une interface qui déclare ses méthodes métier. Vous devriez alors spécifier ces interfaces dans le fichier de mapping :

<class name="eg.Cat" proxy="eg.ICat">
    ......
    <subclass name="eg.DomesticCat" proxy="eg.IDomesticCat">
        .....
    </subclass>
</class>

Cat implémente l'interface ICat et DomesticCat implémente l'interface IDomesticCat. Ainsi, des proxys pour les instances de Cat et DomesticCat pourraient être retournées par load() ou iterate() (Notez que find() ne retourne pas de proxy).

ICat cat = (ICat) session.load(Cat.class, catid);
Iterator iter = session.iterate("from cat in class eg.Cat where cat.name='fritz'");
ICat fritz = (ICat) iter.next();

Les relations sont aussi initialisées tardivement. Ceci signifie que vous devez déclarer chaque propriété comme étant de type ICat, et non Cat.

Certaines opérations ne nécessitent pas l'initialisation du proxy

  • equals(), si la classe persistante ne surcharge pas equals()

  • hashCode(), si la classe persistante ne surcharge pas hashCode()

  • Le getter de l'identifiant

Hibernate détectera les classes qui surchargent equals() ou hashCode().

Les exceptions qui surviennent à l'initialisation d'un proxy sont encapsulées dans une LazyInitializationException.

Parfois, nous devons nous assurer qu'un proxy ou une collection est initialisée avant de fermer la Session. Bien sûr, nous pouvons toujours forcer l'initialisation en appelant par exemple cat.getSex() ou cat.getKittens().size(). Mais ceci n'est pas très lisible pour les personnes parcourant le code et n'est pas très générique. Les méthodes statiques Hibernate.initialize() et Hibernate.isInitialized() fournissent à l'application un moyen de travailler avec des proxys ou des collections initialisés. Hibernate.initialize(cat) forcera l'initialisation d'un proxy de cat, si tant est que sa Session est ouverte. Hibernate.initialize( cat.getKittens() ) a le même effet sur la collection kittens.

14.3. Utiliser le batch fetching (chargement par batch)

Pour améliorer les performances, Hibernate peut utiliser le batch fetching ce qui veut dire qu'Hibernate peut charger plusieurs proxys non initialisés en une seule requête lorsque l'on accède à l'un de ces proxys. Le batch fetching est une optimisation intimement liée à la stratégie de chargement tardif. Il y a deux moyens d'activer le batch fetching : au niveau de la classe et au niveau de la collection.

Le batch fetching pour les classes/entités est plus simple à comprendre. Imaginez que vous ayez la situation suivante à l'exécution : vous avez 25 instances de Cat chargées dans une Session, chaque Cat a une référence à son owner, une Person. La classe Person est mappée avec un proxy, lazy="true". Si vous itérez sur tous les cats et appelez getOwner() sur chacun d'eux, Hibernate exécutera par défaut 25 SELECT, pour charger les owners (initialiser le proxy). Vous pouvez paramétrer ce comportement en spécifiant une batch-size (taille de batch) dans le mapping de Person :

<class name="Person" lazy="true" batch-size="10">...</class>

Hibernate exécutera désormais trois requêtes, en chargeant respectivement 10, 10, et 5 entités. Vous pouvez voir que le batch fetching est une optimisation aveugle dans le mesure où elle dépend du nombre de proxys non initialisés dans une Session particulière.

Vous pouvez aussi activer le batch fetching pour les collections. Par exemple, si chaque Person a une collection chargée tardivement de Cats, et que 10 persons sont actuellement chargées dans la Session, itérer sur toutes les persons générera 10 SELECTs, un pour chaque appel de getCats(). Si vous activez le batch fetching pour la collection cats dans le mapping de Person, Hibernate pourra précharger les collections :

<class name="Person">
    <set name="cats" lazy="true" batch-size="3">
        ...
    </set>
</class>

Avec une taille de batch (batch-size) de 3, Hibernate chargera respectivement 3, 3, 3, et 1 collections en 4 SELECTs. Encore une fois, la valeur de l'attribut dépend du nombre de collections non initialisées dans une Session particulière.

Le batch fetching de collections est particulièrement utile si vous avez des arborescenses récursives d'éléments (typiquement, le schéma facture de matériels).

14.4. Le cache de second niveau

Une Session Hibernate est un cache de niveau transactionnel des données persistantes. Il est possible de configurer un cache de cluster ou de JVM (de niveau SessionFactory pour être exact) défini classe par classe et collection par collection. Vous pouvez même utiliser votr choix de cache en implémentant le pourvoyeur (provider) associé. Faites attention, les caches ne sont jamais avertis des modifications faites dans la base de données par d'autres applications (ils peuvent cependant être configurés pour régulièrement expirer les données en cache).

Par défaut, Hibernate utilise EHCache comme cache de niveau JVM (le support de JCS est désormais déprécié et sera enlevé des futures versions d'Hibernate). Vous pouvez choisir une autre implémentation en spécifiant le nom de la classe qui implémente net.sf.hibernate.cache.CacheProvider en utilisant la propriété hibernate.cache.provider_class.

Tableau 14.1. Fournisseur de cache

CacheClasse pourvoyeuseTypeSupport en ClusterCache de requêtes supporté
Hashtable (ne pas utiliser en production)net.sf.hibernate.cache.HashtableCacheProvidermémoire oui
EHCachenet.sf.hibernate.cache.EhCacheProvidermémoire, disque oui
OSCachenet.sf.hibernate.cache.OSCacheProvidermémoire, disque oui
SwarmCachenet.sf.hibernate.cache.SwarmCacheProvideren cluster (multicast ip)oui (invalidation de cluster) 
JBoss TreeCachenet.sf.hibernate.cache.TreeCacheProvideren cluster (multicast ip), transactionneloui (replication)oui (horloge sync. nécessaire)

14.4.1. Mapping de Cache

L'élément <cache> d'une classe ou d'une collection à la forme suivante :

<cache 
    usage="transactional|read-write|nonstrict-read-write|read-only"  (1)
/>
(1)

usage spécifie la stratégie de cache : transactionel, lecture-écriture, lecture-écriture non stricte ou lecture seule

Alternativement (voir préférentiellement), vous pouvez spécifier les éléments <class-cache> et <collection-cache> dans hibernate.cfg.xml.

L'attribut usage spécifie une stratégie de concurrence d'accès au cache.

14.4.2. Strategie : lecture seule

Si votre application a besoin de lire mais ne modifie jamais les instances d'une classe, un cache read-only peut être utilisé. C'est la stratégie la plus simple et la plus performante. Elle est même parfaitement sûre dans un cluster.

<class name="eg.Immutable" mutable="false">
    <cache usage="read-only"/>
    ....
</class>

14.4.3. Stratégie : lecture/écriture

Si l'application a besoin de mettre à jour des données, un cache read-write peut être approprié. Cette stratégie ne devrait jamais être utilisée si votre application nécessite un niveau d'isolation transactionnelle sérialisable. Si le cache est utilisé dans un environnement JTA, vous devez spécifier hibernate.transaction.manager_lookup_class, fournissant une stratégie pour obtenir le TransactionManager JTA. Dans d'autres environnements, vous devriez vous assurer que la transation est terminée à l'appel de Session.close() ou Session.disconnect(). Si vous souhaitez utiliser cette stratégie dans un cluster, vous devriez vous assurer que l'implémentation de cache utilisée supporte le vérrouillage. Ce que ne font pas les pourvoyeurs caches fournis.

<class name="eg.Cat" .... >
    <cache usage="read-write"/>
    ....
    <set name="kittens" ... >
        <cache usage="read-write"/>
        ....
    </set>
</class>

14.4.4. Stratégie : lecture/écriture non stricte

Si l'application besoin de mettre à jour les données de manière occasionnelle (qu'il est très peu probable que deux transactions essaient de mettre à jour le même élément simultanément) et qu'une isolation transactionnelle stricte n'est pas nécessaire, un cache nonstrict-read-write peut être approprié. Si le cache est utilisé dans un environnement JTA, vous devez spécifier hibernate.transaction.manager_lookup_class. Dans d'autres environnements, vous devriez vous assurer que la transation est terminée à l'appel de Session.close() ou Session.disconnect()

14.4.5. Stratégie : transactionelle

La stratégie de cache transactional supporte un cache complètement transactionnel comme, par exemple, JBoss TreeCache. Un tel cache ne peut être utilisé que dans un environnement JTA et vous devez spécifier hibernate.transaction.manager_lookup_class.

Aucun des caches livrés ne supporte toutes les stratégies de concurrence. Le tableau suivant montre quels caches sont compatibles avec quelles stratégies de concurrence.

Tableau 14.2. Stratégie de concurrence du cache

Cacheread-only (lecture seule)nonstrict-read-write (lecture-écriture non stricte)read-write (lecture-ériture)transactional (transactionnel)
Hashtable (ne pas utilser en production)ouiouioui 
EHCacheouiouioui 
OSCacheouiouioui 
SwarmCacheouioui  
JBoss TreeCacheoui  oui

14.5. Gérer le cache de la Session

A chaque fois que vous passez un objet à save(), update() ou saveOrUpdate() ou chaque fois que récupérez un objet via load(), find(), iterate(), ou filter(), cet objet est ajouté au cache interne de la Session. Quand flush() est appelé, l'état de cet objet est synchronisé avec la base de données. Si vous ne souhaitez pas que cette synchronisation se fasse ou si vous êtes en train de travailler avec un grand nombre d'objets et avez besoin de gérer la mémoire de manière efficace, la méthode evict() peut être utilisée pour enlever l'objet et ses collections du cache.

Iterator cats = sess.iterate("from eg.Cat as cat"); //un grand result set
while ( cats.hasNext() ) {
    Cat cat = (Cat) iter.next();
    doSomethingWithACat(cat);
    sess.evict(cat);
}

Hibernate enlèvera automatiquement toutes les entités associées si l'association est mappée avec cascade="all" ou cascade="all-delete-orphan".

La Session dispose aussi de la méthode contains() pour déterminer si une instance appartient au cache de la session.

Pour retirer tous les objets du cache session, appelez Session.clear()

Pour le cache de second niveau, il existe des méthodes définies dans SessionFactory pour retirer des instances du cache, la classe entière, une instance de collection ou le rôle entier d'une collection.

14.6. Le cache de requêtes

Les résultats d'une requête peuvent aussi être placés en cache. Ceci n'est utile que pour les requêtes qui sont exécutées avec les mêmes paramètres. Pour utiliser le cache de requêtes, vous devez d'abord l'activer en mettant hibernate.cache.use_query_cache=true. Ceci active la création de deux régions de cache, une contenant les résultats des requêtes en cache (net.sf.hibernate.cache.QueryCache), l'autre contenant les modifications les plus récentes des tables interrogées (net.sf.hibernate.cache.UpdateTimestampsCache). Notez que le cache de requête ne met pas en cache l'état de chaque entité du résultat, il met seuleument en cache les valeurs des identifiants et les résultats de type valeur. Le cache requête est donc généralement utilisé en association avec le cache de second niveau.

La plupart des requêtes ne retirent pas de bénéfice pas du cache, donc par défaut les requêtes ne sont pas mises en cache. Pour activer le cache, appelez Query.setCacheable(true). Cet appel permet de vérifier si les résultats sont en cache ou non, voire d'ajouter ces résultats si la requête est exécutée.

Si vous avez besoin de contrôler finement les délais d'expiration du cache, vous pouvez spécifier une région de cache nommée pour une requête particulière en appelant Query.setCacheRegion().

List blogs = sess.createQuery("from Blog blog where blog.blogger = :blogger")
    .setEntity("blogger", blogger)
    .setMaxResults(15)
    .setCacheable(true)
    .setCacheRegion("frontpages")
    .list();

Si une requête doit forcer le rafraîchissement de sa région de cache, vous pouvez forcer Query.setForceCacheRefresh() à true. C'est particulièrement utile dans les cas ou la base de données peut être mise à jour par un autre processus (autre qu'Hibernate) et permet à l'application de rafraichir de manière sélective les régions de cache de requête en fonction de sa connaissance des évènements. C'est une alternative à l'éviction d'une région de cache de requête. Si vous avez besoin d'un contrôle fin du rafraîchissement pour plusieurs requêtes, utlisez cette fonction plutôt qu'une nouvelle région pour chaque requête.