Améliorer les performances
Stratégies de chargement
Une stratégie de chargement est une stratégie qu'Hibernate va
utiliser pour récupérer des objets associés si l'application à besoin de naviguer à
travers une association.
Les stratégies de chargement peuvent être déclarées dans les méta-données de l'outil
de mapping objet relationnel ou surchargées par une requête de type HQL ou Criteria
particulière.
Hibernate3 définit les stratégies de chargement suivantes :
Chargement par jointure - Hibernate récupère
l'instance associée ou la collection dans un même SELECT,
en utilisant un OUTER JOIN.
Chargement par select - Un second SELECT
est utilisé pour récupérer l'instance associée ou la collection. A moins
que vous ne désactiviez explicitement le chargement tardif en spécifiant
lazy="false", ce second select ne sera exécuté que lorsque
vous accéderez réellement à l'association.
Chargement par sous-select - Un second SELECT
est utilisé pour récupérer les associations pour toutes les entités récupérées dans
une requête ou un chargement préalable. A moins
que vous ne désactiviez explicitement le chargement tardif en spécifiant
lazy="false", ce second select ne sera exécuté que lorsque
vous accéderez réellement à l'association.
Chargement par lot - Il s'agit d'une stratégie d'optimisation
pour le chargement par select - Hibernate récupère un lot
d'instances ou de collections en un seul SELECT en spécifiant
une liste de clé primaire ou de clé étrangère.
Hibernate fait également la distinction entre :
Chargement immédiat - Une association, une collection ou
un attribut est chargé immédiatement lorsque l'objet auquel appartient cet
élément est chargé.
Chargement tardif d'une collection - Une collection est
chargée lorque l'application invoque une méthode sur cette collection (il s'agit
du mode de chargement par défaut pour les collections).
Chargement "super tardif" d'une collection - les
éléments de la collection sont récupérés individuellement depuis la base de données
lorsque nécessaire.
Hibernate essaie de ne pas charger toute la collection en mémoire sauf si cela est
absolument nécessaire (bien adapté aux très grandes collections).
Chargement par proxy - une association vers un seul
objet est chargée lorsqu'une méthode autre que le getter sur l'identifiant est
appelée sur l'objet associé.
Chargement "sans proxy" - une association vers un seul objet
est chargée lorsque l'on accède à cet objet. Par rapport au chargement par proxy,
cette approche est moins tardif (l'association est quand même chargée même
si on n'accède qu'à l'identifiant) mais plus transparente car il n'y a pas de proxy
visible dans l'application. Cette approche requiert une instrumentation du bytecode
à la compilation et est rarement nécessaire.
Chargement tardif des attributs - Un attribut ou un
objet associé seul est chargé lorsque l'on y accède. Cette approche requiert
une instrumentation du bytecode à la compilation et est rarement nécessaire.
Nous avons ici deux notions orthogonales : quand l'association est
chargée et comment (quelle requête SQL est utilisée). Il ne faut
pas confondre les deux. Le mode de chargement est utilisé pour améliorer les performances.
On peut utiliser le mode tardif pour définir un contrat sur quelles données sont toujours
accessibles sur une instance détachée d'une classe particulière.
Travailler avec des associations chargées tardivement
Par défaut, Hibernate3 utilise le chargement tardif par select pour les collections
et le chargement tardif par proxy pour les associations vers un seul objet.
Ces valeurs par défaut sont valables pour la plupart des associations dans la
plupart des applications.
Note : si vous définissez
hibernate.default_batch_fetch_size, Hibernate va utiliser l'optimisation
du chargement par lot pour le chargement tardif (cette optimisation peut aussi
être activée à un niveau de granularité plus fin).
Cependant, le chargement tardif pose un problème qu'il faut connaitre. L'accès à
une association définie comme "tardive", hors du contexte d'une session hibernate
ouverte, va conduire à une exception. Par exemple :
Etant donné que la collection des permissions n'a pas été initialisée
avant que la Session soit fermée, la collection n'est
pas capable de se charger. Hibernate ne supporte pas le chargement
tardif pour des objets détachés. La solution à ce problème est de
déplacer le code qui lit la collection avant le "commit" de la transaction.
Une autre alternative est d'utiliser une collection ou une association non
"tardive" en spécifiant lazy="false" dans le mapping de
l'association.
Cependant il est prévu que le chargement tardif soit utilisé pour quasiment
toutes les collections ou associations. Si vous définissez trop d'associtions
non "tardives" dans votre modèle objet, Hibernate va finir par devoir charger
toute la base de données en mémoire à chaque transaction !
D'un autre côté, on veut souvent choisir un chargement par jointure (qui est par
défaut non tardif) à la place du chargement par select dans une transaction particulière.
Nous allons maintenant voir comment adapter les stratégies de chargement. Dans Hibernate3
les mécanismes pour choisir une stratégie de chargement sont identiques que
l'on ait une association vers un objet simple ou vers une collection.
Personnalisation des stratégies de chargement
Le chargement par select (mode par défaut) est très vulnérable au problème du
N+1 selects, du coup vous pouvez avoir envie d'activer le chargement par jointure
dans les fichiers de mapping :
]]>
La stratégie de chargement définie à l'aide du mot fetch dans les fichiers
de mapping affecte :
La récupération via get() ou load()
La récupération implicite lorsque l'on navigue à travers une association
Les requêtes de type Criteria
Les requêtes HQL si l'on utilise le chargement par subselect
Quelle que soit la stratégie de chargement que vous utilisez, la partie du graphe
d'objets qui est définie comme non "tardive" sera chargée en mémoire. Cela peut
mener à l'exécution de plusieurs selects successifs pour une seule requête HQL.
On n'utilise pas souvent les documents de mapping pour adapter le chargement.
Au lieu de cela, on conserve le comportement par défaut et on le surcharge pour
une transaction particulière en utilisant left join fetch
dans les requêtes HQL. Cela indique à hibernate à Hibernate de charger l'association
de manière agressive lors du premier select en utilisant une jointure externe.
Dans l'API Criteria vous pouvez utiliser la méthode
setFetchMode(FetchMode.JOIN)
Si vous ne vous sentez pas prêt à modifier la stratégie de chargement utilisé
par get() ou load(), vous pouvez juste
utiliser une requête de type Criteria comme par exemple :
(Il s'agit de l'équivalent pour Hibernate de ce que d'autres outils de mapping
appellent un "fetch plan" ou "plan de chargement")
Une autre manière complètement différente d'éviter le problème des N+1 selects
est d'utiliser le cache de second niveau.
Proxys pour des associations vers un seul objet
Le chargement tardif des collections est implémenté par Hibernate en utilisant
ses propres implémentations pour des collections persistantes. Si l'on veut un
chargement tardif pour des associations vers un seul objet métier il faut utiliser
un autre mécanisme. L'entité qui est pointée par l'association doit être masquée
derrière un proxy. Hibernate implémente l'initialisation tardive des proxys sur des
objets persistents via une mise à jour à chaud du bytecode (à l'aide de l'excellente
librairie CGLIB).
Par défaut, Hibernate génère des proxys (au démarrage) pour toutes les classes
persistantes et les utilise pour activer le chargement tardif des associations
many-to-one et one-to-one.
Le fichier de mapping peut déclarer une interface qui sera utilisée par le proxy
d'interfaçage pour cette classe à l'aide de l'attribut proxy.
Par défaut Hibernate utilises une sous classe de la classe persistante.
Il faut que les classes pour lesquelles on ajoute un proxy implémentent
un constructeur par défaut de visibilité au moins package. Ce constructeur est
recommandé pour toutes les classes persistantes !
Il y a quelques précautions à prendre lorsque l'on étend cette approche à des classes
polymorphiques, exemple :
......
.....
]]>
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 :
Deuxièmement, il est possible de casser la notion d'== des proxy.
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 :
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 :
......
.....
]]>
où CatImpl implémente l'interface Cat et DomesticCatImpl
implémente l'interface DomesticCat. Ainsi, des proxys pour les instances de
Cat et DomesticCat pourraient être retournées par load()
ou iterate() (Notez que list() ne retourne généralement pas de proxy).
Les relations sont aussi initialisées tardivement. Ceci signifie que vous
devez déclarer chaque propriété comme étant de type Cat,
et non CatImpl.
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().
Eh choisissant lazy="no-proxy" au lieu de lazy="proxy"
qui est la valeur par défaut, il est possible d'éviter les problèmes liés au transtypage.
Il faudra alors une instrumentation du bytecode à la compilation et toutes les opérations
résulterons immédiatement en une initialisation du proxy.
Initialisation des collections et des proxys
Une exception de type LazyInitializationException sera renvoyée par hibernate
si une collection ou un proxy non initialisé est accédé en dehors de la portée de la Session,
e.g. lorsque l'entité à laquelle appartient la collection ou qui a une référence vers le proxy est
dans l'état "détachée".
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.
Une autre option est de conserver la Session ouverte jusqu'à
ce que toutes les collections et tous les proxys aient été chargés. Dans certaines
architectures applicatives, particulièrement celles ou le code d'accès aux données
via hiberante et le code qui utilise ces données sont dans des couches applicatives
différentes ou des processus physiques différents, il peut devenir problématique
de garantir que la Session est ouverte lorsqu'une collection
est initialisée. Il y a deux moyens de traiter ce problème :
Dans une application web, un filtre de servlet peut être utilisé pour
fermer la Session uniquement lorsque la requête
a été entièrement traitée, lorsque le rendu de la vue est fini
(il s'agit du pattern Open Session in View).
Bien sûr, cela demande plus d'attention à la bonne gestion des exceptions
de l'application. Il est d'une importance vitale que la Session
soit fermée et la transaction terminée avant que l'on rende la main à l'utilisateur
même si une exception survient durant le traitement de la vue.
Voir le wiki Hibernate pour des exemples sur le pattern
"Open Session in View".
Dans une application avec une couche métier séparée, la couche contenant
la logique métier doit "préparer" toutes les collections qui seront
nécessaires à la couche web avant de retourner les données. Cela signifie
que la couche métier doit charger toutes les données et retourner toutes
les données déjà initialisées à la couche de présentation/web pour un
cas d'utilisation donné. En général l'application appelle la méthode
Hibernate.initialize() pour chaque collection nécessaire
dans la couche web (cet appel doit être fait avant la fermeture de la session)
ou bien récupère les collections de manière agressive à l'aide d'une requête
HQL avec une clause FETCH ou à l'aide du mode
FetchMode.JOIN pour une requête de type Criteria.
Cela est en général plus facile si vous utilisez le pattern Command
plutôt que Session Facade.
Vous pouvez également attacher à une Session un objet chargé
au préalable à l'aide des méthodes merge() ou lock()
avant d'accéder aux collections (ou aux proxys) non initialisés. Non, Hibernate ne
fait pas, et ne doit pas faire, cela automatiquement car cela pourrait introduire
une sémantique transactionnelle ad hoc.
Parfois, vous ne voulez pas initialiser une grande collection mais vous avez quand même
besoin d'informations sur elle (comme sa taille) ou un sous ensemble de ses données
Vous pouvez utiliser un filtre de collection pour récupérer sa taille sans l'initialiser :
La méthode createFilter() est également utilisée pour récupérer
de manière efficace des sous ensembles d'une collection sans avoir besoin de l'initialiser
dans son ensemble.
Utiliser le chargement par lot
Pour améliorer les performances, Hibernate peut utiliser le chargement par lot
ce qui veut dire qu'Hibernate peut charger plusieurs proxys (ou collections) non initialisés en une seule
requête lorsque l'on accède à l'un de ces proxys. Le chargement par lot est une optimisation
intimement liée à la stratégie de chargement tardif par select. Il y a deux moyens d'activer le
chargement par lot : au niveau de la classe et au niveau de la collection.
Le chargement par lot 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 du lot) dans le mapping de Person :
...]]>
Hibernate exécutera désormais trois requêtes, en chargeant respectivement 10,
10, et 5 entités.
Vous pouvez aussi activer le chargement par lot pour les collections. Par exemple,
si chaque Person a une collection chargée tardivement de
Cats, et que 10 personnes 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 chargement par lot pour la
collection cats dans le mapping de Person, Hibernate pourra
précharger les collections :
...
]]>
Avec une taille de lot (batch-size) de 8, Hibernate chargera
respectivement 3, 3, 3, et 1 collections en quatre SELECTs.
Encore une fois, la valeur de l'attribut dépend du nombre de collections
non initialisées dans une Session particulière.
Le chargement par lot de collections est particulièrement utile si vous avez des
arborescenses récursives d'éléments (typiquement, le schéma facture de
matériels). (Bien qu'un sous ensemble ou un
chemin matérialisé est sans doute une meilleure option pour
des arbres principalement en lecture.)
Utilisation du chargement par sous select
Si une collection ou un proxy vers un objet doit être chargé, Hibernate va tous les
charger en ré-exécutant la requête orignial dans un sous select. Cela fonctionne de la
même manière que le chargement par lot sans la possibilité de fragmenter le chargement.
Utiliser le chargement tardif des propriétés
Hibernate3 supporte le chargement tardif de propriétés individuelles. La technique
d'optimisation est également connue sous le nom de fetch groups (groupes
de chargement). Il faut noter qu'il s'agit principalement d'une fonctionnalité marketing
car en pratique l'optimisation de la lecture d'un enregistrement est beaucoup plus importante
que l'optimisation de la lecture d'une colonne. Cependant, la restriction du chargement à
certaines colonnes peut être pratique dans des cas extrèmes, lorsque des tables "legacy"
possèdent des centaines de colonnes et que le modèle de données ne peut pas être amélioré.
Pour activer le chargement tardif d'une propriété, il faut mettre l'attribut lazy
sur une propriété particulière du mapping :
]]>
Le chargement tardif des propriétés requiert une instrumentation du bytecode lors de la
compilation ! Si les classes persistantes ne sont pas instrumentées, Hibernate ignorera de
manière silencieuse le mode tardif et retombera dans le mode de chargement immédiat.
Pour l'instrumentation du bytecode vous pouvez utiliser la tâche Ant suivante :
]]>
Une autre façon (meilleure ?) pour éviter de lire plus de colonnes que
nécessaire au moins pour des transactions en lecture seule est d'utiliser
les fonctionnalités de projection des requêtes HQL ou Criteria. Cela évite
de devoir instrumenter le bytecode à la compilation et est certainement une
solution préférable.
Vous pouvez forcer le mode de chargement agressif des propriétés en utilisant
fetch all properties dans les requêts HQL.
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 org.hibernate.cache.CacheProvider en utilisant
la propriété hibernate.cache.provider_class.
Fournisseur de cache
Cache
Classe pourvoyeuse
Type
Support en Cluster
Cache de requêtes supporté
Hashtable (ne pas utiliser en production)
org.hibernate.cache.HashtableCacheProvider
mémoire
oui
EHCache
org.hibernate.cache.EhCacheProvider
mémoire, disque
oui
OSCache
org.hibernate.cache.OSCacheProvider
mémoire, disque
oui
SwarmCache
org.hibernate.cache.SwarmCacheProvider
en cluster (multicast ip)
oui (invalidation de cluster)
JBoss TreeCache
org.hibernate.cache.TreeCacheProvider
en cluster (multicast ip), transactionnel
oui (replication)
oui (horloge sync. nécessaire)
Mapping de Cache
L'élément <cache> d'une classe ou d'une collection à
la forme suivante :
]]>
usage (requis) spécifie la stratégie de cache :
transactionel,
lecture-écriture,
lecture-écriture non stricte ou
lecture seule
region (optionnel, par défaut il s'agit du nom
de la classe ou du nom de role de la collection) spécifie le nom de la
région du cache de second niveau
include (optionnel, par défaut all)
non-lazy spécifie que les propriétés des entités mappées avec
lazy="true" ne doivent pas être mises en cache lorsque
le chargement tardif des attributs est activé.
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.
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.
....
]]>
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.
....
....
]]>
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()
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.
Stratégie de concurrence du cache
Cache
read-only (lecture seule)
nonstrict-read-write (lecture-écriture non stricte)
read-write (lecture-ériture)
transactional (transactionnel)
Hashtable (ne pas utilser en production)
oui
oui
oui
EHCache
oui
oui
oui
OSCache
oui
oui
oui
SwarmCache
oui
oui
JBoss TreeCache
oui
oui
Gérer les caches
A chaque fois que vous passez un objet à la méthode save(),
update() ou saveOrUpdate() et à chaque fois
que vous récupérez un objet avec load(), get(),
list(), iterate() or scroll(),
cet objet est ajouté au cache interne de la Session.
Lorsqu'il y a un appel à la méthode flush(), l'état de cet objet
va être synchronisé avec la base de données. Si vous ne voulez pas que cette synchronisation
ait lieu ou si vous traitez un grand nombre d'objets et que vous avez besoin de gérer
la mémoire de manière efficace, vous pouvez utiliser la méthode evict()
pour supprimer l'objet et ses collections dépendantes du cache de la session
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.
Le CacheMode contrôle comme une session particulière interragit avec le
cache de second niveau
CacheMode.NORMAL - lit et écrit les items dans le cache de second niveau
CacheMode.GET - lit les items dans le cache de second niveau mais ne
les écrit pas sauf dans le cache d'une mise à jour d'une donnée
CacheMode.PUT - écrit les items dans le cache de second niveau mais ne les
lit pas dans le cache de second niveau
CacheMode.REFRESH - écrit les items dans le cache de second niveau mais ne les
lit pas dans le cache de second niveau, outrepasse l'effet dehibernate.cache.use_minimal_puts,
en forçant un rafraîchissement du cache de second niveau pour chaque item lu dans la base
Pour parcourir le contenu du cache de second niveau ou la région du cache dédiée au requêtes, vous
pouvez utiliser l'API Statistics
API:
Vous devez pour cela activer les statistiques et optionnellement forcer Hibernate à conserver les entrées dans le
cache sous un format plus compréhensible pour l'utilisateur :
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 :
Ce paramètre amène la création de deux nouvelles régions dans le cache, une qui va conserver
le résultat des requêtes mises en cache (org.hibernate.cache.StandardQueryCache)
et l'autre qui va conserver l'horodatage des mises à jour les plus récentes effectuées sur les
tables requêtables (org.hibernate.cache.UpdateTimestampsCache).
Il faut noter que le cache de requête ne conserve pas l'état des entités, il met en cache
uniquement les valeurs de l'identifiant et les valeurs de types de base (?). Le cache
de requête doit toujours être utilisé avec le cache de second niveau pour être efficace.
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().
Si une requête doit forcer le rafraîchissement de sa région de cache, vous devez
appeler Query.setCacheMode(CacheMode.REFRESH). C'est particulièrement
utile lorsque les données peuvent avoir été mises à jour par un processus séparé (e.g. elles
n'ont pas été modifiées par Hibernate). Cela permet à l'application de rafraîchir de
manière sélective les résultats d'une requête particulière. Il s'agit d'une alternative plus
efficace à l'éviction d'une région du cache à l'aide de la méthode
SessionFactory.evictQueries().
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.
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 mapping à l'aide d'<idbag> définit une clé
de substitution ce qui leur permet d'être très efficaces lors de la
mise à jour. En fait il s'agit du meilleur cas de mise à jour d'une collection
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).
Les lists, les maps, les idbags 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 idbags sont les types de collections
(non inversées) les plus performants, avec les sets pas loin derrières.
Les sets son le type de collection le plus courant dans les applications Hibernate. Cela
est du au fait que la sémantique des "set" est la plus naturelle dans le modèle
relationnel.
Cependant, dans des modèles objet bien conçus avec Hibernate, on voit souvent que
la plupart des collections sont en fait des associations "un-vers-plusieurs" avec
inverse="true". Pour ces associations, les mises à jour sont gérées
au niveau de l'association "plusieurs-vers-un" et les considérations de performance de
mise à jour des collections ne s'appliquent tout simplement pas dans ces cas là.
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.
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.
Bien sûr, la suppression en un coup ne s'applique pas pour les collections qui sont mappées
avec inverse="true".
Moniteur de performance
L'optimisation n'est pas d'un grand intérêt sans le suivi et l'accès aux données de
performance. Hibernate fournit toute une panoplie de rapport sur ses opérations internes.
Les statistiques dans Hibernate sont fournies par SessionFactory.
Suivi d'une SessionFactory
Vous pouvez accéder au métriques d'une SessionFactory de deux
manières. La première option est d'appeler sessionFactory.getStatistics()
et de lire ou d'afficher les Statistics vous même.
Hibernate peut également utiliser JMX pour publier les métriques si vous activez
le MBean StatisticsService. Vous pouvez activer un seul MBean
pour toutes vos SessionFactory ou un par factory. Voici un code
qui montre un exemple de configuration minimaliste :
TODO: Cela n'a pas de sens : dans le premier cs on récupère et on utilise le MBean directement.
Dans le second, on doit fournir le nom JNDI sous lequel est retenu la fabrique de session avant de
l'utiliser. Pour cela il faut utiliser
hibernateStatsBean.setSessionFactoryJNDIName("my/JNDI/Name")
Vous pouvez (dés)activer le suivi pour une SessionFactory
au moment de la configuration en mettant hibernate.generate_statistics à false
à chaud avec sf.getStatistics().setStatisticsEnabled(true)
ou hibernateStatsBean.setStatisticsEnabled(true)
Les statistiques peuvent être remises à zéro de manière programmatique à l'aide de la méthode
clear()
Un résumé peut être envoyé à un logger (niveau info) à l'aide de la méthode logSummary()
Métriques
Hibernate fournit un certain nombre de métriques, qui vont des informations très basiques
aux informations très spécialisées qui ne sont appropriées que dans certains scenarii.
Tous les compteurs accessibles sont décrits dans l'API de l'interface
Statistics dans trois catégories :
Les métriques relatives à l'usage général de la Session
comme le nombre de sessions ouvertes, le nombre de connexions JDBC récupérées, etc...
Les métriques relatives aux entités, collections, requêtes et caches dans
leur ensemble (métriques globales),
Les métriques détaillées relatives à une entité, une collection, une requête
ou une région de cache particulière.
Par exemple, vous pouvez vérifier l'accès au cache ainsi que le taux d'éléments manquants et
de mise à jour des entités, collections et requêtes et le temps moyen que met une requête.
Il faut faire attention au fait que le nombre de millisecondes est sujet à approximation en
Java. Hibernate est lié à la précision de la machine virtuelle, sur certaines plateformes,
cela n'offre qu'une précision de l'ordre de 10 secondes.
Des accesseurs simples sont utilisés pour accéder aux métriques globales (e.g. celles qui ne
sont pas liées à une entité, collection ou région de cache particulière). Vous pouvez accéder
aux métriques d'une entité, collection, région de cache particulière à l'aide de son nom et à l'aide
de sa représentation HQL ou SQL pour une requête. Référez vous à la javadoc des APIS
Statistics, EntityStatistics,
CollectionStatistics, SecondLevelCacheStatistics,
and QueryStatistics pour plus d'informations. Le code ci-dessous montre
un exemple simple :
Pour travailler sur toutes les entités, collections, requêtes et régions de cache, vous pouvez
récupérer la liste des noms des entités, collections, requêtes et régions de cache avec les
méthodes : getQueries(), getEntityNames(),
getCollectionRoleNames(), et
getSecondLevelCacheRegionNames().