Hibernate.orgCommunity Documentation
Une stratégie de chargement est une stratégie que Hibernate va utiliser pour récupérer des objets associés si l'application a 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 mappage 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 à l'entité ou à la collection. À moins que vous ne désactiviez explicitement le chargement différé 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 différé 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és primaires ou de clés étrangères.
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 différé d'une collection - Une collection est chargée lorsque l'application invoque une méthode sur cette collection (il s'agit du mode de chargement par défaut pour les collections).
Chargement "super différé" d'une collection - On accède aux éléments de la collection depuis la base de données lorsque c'est 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 à la variable d'instance. Par rapport au chargement par proxy, cette approche est moins différée (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 différé des attributs - Un attribut ou un objet associé seul est chargé lorsque l'on accède à la variable d'instance. 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 les confondre. Le mode de chargement fetch
est utilisé pour améliorer les performances. On peut utiliser le mode lazy
pour définir un contrat sur quelles données sont toujours accessibles sur toute instance détachée d'une classe particulière.
Par défaut, Hibernate3 utilise le chargement différé par select pour les collections et le chargement différé 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.
Si vous définissez hibernate.default_batch_fetch_size
, Hibernate va utiliser l'optimisation du chargement par lot pour le chargement différé (cette optimisation peut aussi être activée à un niveau de granularité plus fin).
L'accès à une association définie comme "différé", hors du contexte d'une session Hibernate ouverte, entraîne une exception. Par exemple :
s = sessions.openSession();
Transaction tx = s.beginTransaction();
User u = (User) s.createQuery("from User u where u.name=:userName")
.setString("userName", userName).uniqueResult();
Map permissions = u.getPermissions();
tx.commit();
s.close();
Integer accessLevel = (Integer) permissions.get("accounts"); // Error!
Étant donné que la collection des permissions n'a pas été initialisée avant de fermer la Session
, la collection n'est pas capable de charger son état. Hibernate ne supporte pas le chargement différé pour des objets détachés. La solution à ce problème est de déplacer le code qui lit à partir de la collection avant le "commit" de la transaction.
Une autre alternative est d'utiliser une collection ou une association non "différée" en spécifiant lazy="false"
dans le mappage de l'association. Cependant il est prévu que le chargement différé soit utilisé pour quasiment toutes les collections ou associations. Si vous définissez trop d'associations non "différées" dans votre modèle objet, Hibernate va finir par devoir charger toute la base de données en mémoire à chaque transaction.
Par ailleurs, on veut souvent choisir un chargement par jointure (qui est par défaut non différé) à 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.
Le chargement par select (mode par défaut) est très vulnérable au problème du N+1 selects, ainsi vous souhaiterez peut-être activer le chargement par jointure dans les fichiers de mappage :
<set name="permissions"
fetch="join">
<key column="userId"/>
<one-to-many class="Permission"/>
</set
<many-to-one name="mother" class="Cat" fetch="join"/>
La stratégie de chargement définie à l'aide du mot fetch
dans les fichiers de mappage affecte :
La récupération via get()
ou load()
La récupération implicite lorsque l'on navigue à travers une association
Les requêtes par 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, non-différée, 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 mappage pour adapter le chargement. En revanche, 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 de charger l'association de manière agressive lors du premier select en utilisant une jointure externe. Dans la requête API Criteria
vous utiliserez la méthode setFetchMode(FetchMode.JOIN)
.
S'il vous arrive de vouloir changer la stratégie de chargement utilisée par utilisée par get()
ou load()
, vous pouvez juste utiliser une requête de type Criteria
comme par exemple :
User user = (User) session.createCriteria(User.class)
.setFetchMode("permissions", FetchMode.JOIN)
.add( Restrictions.idEq(userId) )
.uniqueResult();
Il s'agit de l'équivalent pour Hibernate de ce que d'autres outils de mappage 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.
Le chargement différé des collections est implémenté par Hibernate qui utilise ses propres implémentations pour des collections persistantes. Si l'on veut un chargement différé pour des associations vers un seul objet, 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 différée des proxies sur des objets persistants via une mise à jour à chaud du bytecode (à l'aide de l'excellente librairie CGLIB).
Par défaut, Hibernate génère des proxies (au démarrage) pour toutes les classes persistantes et les utilise pour activer le chargement différé des associations many-to-one
et one-to-one
.
Le fichier de mappage peut déclarer une interface à utiliser comme interface de proxy pour cette classe à l'aide de l'attribut proxy
. Par défaut Hibernate utilise 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 avec au minimum une visibilité de paquetage. 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, par exemple :
<class name="Cat" proxy="Cat">
......
<subclass name="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); // instantiate a proxy (does not hit the db)
if ( cat.isDomesticCat() ) { // hit the db to initialize the proxy
DomesticCat dc = (DomesticCat) cat; // Error!
....
}
Deuxièmement, il est possible de casser la notion de ==
des proxies.
Cat cat = (Cat) session.load(Cat.class, id); // instantiate a Cat proxy
DomesticCat dc =
(DomesticCat) session.load(DomesticCat.class, id); // acquire new DomesticCat proxy!
System.out.println(cat==dc); // false
Cette situation n'est pas si mauvaise qu'il n'y parait. Même si nous avons deux références à deux objets proxies différents, l'instance sous-jacente sera quand même le même objet :
cat.setWeight(11.0); // hit the db to initialize the 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 quelconque ressource à l'instanciation (par exemple dans les initialiseurs ou dans le constructeur par défaut), alors ces ressources seront aussi obtenues par le proxy. La classe proxy est en réalité 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 mappage : CatImpl
implémente l'interface Cat
et DomesticCatImpl
implémente l'interface DomesticCat
. Par exemple :
<class name="CatImpl" proxy="Cat">
......
<subclass name="DomesticCatImpl" proxy="DomesticCat">
.....
</subclass>
</class
>
Tout d'abord, les instances de Cat
et de DomesticCat
peuvent être retournées par load()
ou par iterate()
.
Cat cat = (Cat) session.load(CatImpl.class, catid);
Iterator iter = session.createQuery("from CatImpl as cat where cat.name='fritz'").iterate();
Cat fritz = (Cat) iter.next();
list()
ne retourne pas les proxies normalement.
Les relations sont aussi initialisées en différé. 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()
La méthode 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ésulteront immédiatement en une initialisation du proxy.
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
, par ex. lorsque l'entité à laquelle appartient la collection ou qui a une référence vers le proxy, est dans l'état "détaché".
Parfois, nous devons nous assurer qu'un proxy ou une collection est initialisé 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 approprié pour le code générique.
Les méthodes statiques Hibernate.initialize()
et Hibernate.isInitialized()
fournissent à l'application un moyen de travailler avec des proxies 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 proxies nécessaires 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 sera alors difficile de garantir que la Session
est ouverte lorsqu'une collection est initialisée. Il y a deux moyens de maîtriser 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 modèle Vue de la session ouverte). 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 modèle "Open Session in View".
Dans une application avec une couche métier multiniveaux séparée, la couche contenant la logique métier doit "préparer" toutes les collections qui seront nécessaires à la couche web multiniveaux 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 modèle 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 proxies) 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 :
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
La méthode createFilter()
est également utilisée pour récupérer efficacement des sous-ensembles d'une collection sans avoir besoin de l'initialiser dans son ensemble :
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
Pour améliorer les performances, Hibernate peut utiliser le chargement par lot ce qui veut dire que Hibernate peut charger plusieurs proxies (ou collections) non initialisés en une seule requête lorsque l'on accède à l'un de ces proxies. Le chargement par lot est une optimisation intimement liée à la stratégie de chargement en différé 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 mappage de Person
:
<class name="Person" batch-size="10"
>...</class
>
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 en différé des Cat
s, et que 10 personnes sont actuellement chargées dans la Session
, itérer sur toutes les persons générera 10 SELECT
s, un pour chaque appel de getCats()
. Si vous activez le chargement par lot pour la collection cats
dans le mappage de Person
, Hibernate pourra précharger les collections :
<class name="Person">
<set name="cats" batch-size="3">
...
</set>
</class
>
Avec une taille de lot batch-size
de 8, Hibernate chargera respectivement des collections 3, 3, 3, et 1 en quatre SELECT
s. 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 une arborescence imbriquée d'éléments, c'est-à-dire le le schéma facture de matériels typique. (Bien qu'un sous ensemble ou un chemin matérialisé soit probablement une meilleure option pour des arbres principalement en lecture.)
Si une collection en différé ou un proxy vers un objet doit être chargée, Hibernate va tous les charger en ré-exécutant la requête originale dans un sous select. Cela fonctionne de la même manière que le chargement par lot sans la possibilité de fragmenter le chargement.
Another way to affect the fetching strategy for loading associated objects is through something called a fetch profile, which is a named configuration associated with the org.hibernate.SessionFactory
but enabled, by name, on the org.hibernate.Session
. Once enabled on a org.hibernate.Session
, the fetch profile wull be in affect for that org.hibernate.Session
until it is explicitly disabled.
So what does that mean? Well lets explain that by way of an example. Say we have the following mappings:
<hibernate-mapping>
<class name="Customer">
...
<set name="orders" inverse="true">
<key column="cust_id"/>
<one-to-many class="Order"/>
</set>
</class>
<class name="Order">
...
</class>
</hibernate-mapping
>
Now normally when you get a reference to a particular customer, that customer's set of orders will be lazy meaning we will not yet have loaded those orders from the database. Normally this is a good thing. Now lets say that you have a certain use case where it is more efficient to load the customer and their orders together. One way certainly is to use "dynamic fetching" strategies via an HQL or criteria queries. But another option is to use a fetch profile to achieve that. Just add the following to your mapping:
<hibernate-mapping>
...
<fetch-profile name="customer-with-orders">
<fetch entity="Customer" association="orders" style="join"/>
</fetch-profile>
</hibernate-mapping
>
or even:
<hibernate-mapping>
<class name="Customer">
...
<fetch-profile name="customer-with-orders">
<fetch association="orders" style="join"/>
</fetch-profile>
</class>
...
</hibernate-mapping
>
Now the following code will actually load both the customer and their orders:
Session session = ...;
session.enableFetchProfile( "customer-with-orders" ); // name matches from mapping
Customer customer = (Customer) session.get( Customer.class, customerId );
Currently only join style fetch profiles are supported, but they plan is to support additional styles. See HHH-3414 for details.
Hibernate3 supporte le chargement en différé 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 en différé d'une propriété, il faut mettre l'attribut lazy
sur une propriété particulière du mappage :
<class name="Document">
<id name="id">
<generator class="native"/>
</id>
<property name="name" not-null="true" length="50"/>
<property name="summary" not-null="true" length="200" lazy="true"/>
<property name="text" not-null="true" length="2000" lazy="true"/>
</class
>
Le chargement en différé 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 en différé et retombera dans le mode de chargement immédiat.
Pour l'instrumentation du bytecode vous pouvez utiliser la tâche Ant suivante :
<target name="instrument" depends="compile">
<taskdef name="instrument" classname="org.hibernate.tool.instrument.InstrumentTask">
<classpath path="${jar.path}"/>
<classpath path="${classes.dir}"/>
<classpath refid="lib.class.path"/>
</taskdef>
<instrument verbose="true">
<fileset dir="${testclasses.dir}/org/hibernate/auction/model">
<include name="*.class"/>
</fileset>
</instrument>
</target
>
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êtes HQL.
Une Session
Hibernate est un cache de niveau transactionnel de données persistantes. Il est possible de configurer un cache de cluster ou de JVM (de niveau SessionFactory
) défini classe par classe et collection par collection. Vous pouvez même utiliser votre choix de cache en implémentant le fournisseur 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).
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
. Hibernate est accompagné de plusieurs intégrations imbriquées avec des fournisseurs de cache open-source (listés ci-dessous) ; par ailleurs vous pouvez implémenter votre propre fournisseur et le brancher comme indiqué ci-dessus. Notez que les versions antérieures à 3.2, utilisaient par défaut le EhCache comme le fournisseur de cache par défaut, ce qui n'est plus le cas.
Tableau 20.1. Fournisseurs de cache
Cache | Classe pourvoyeuse | Type | Cluster sécurisé | Cache de requêtes supporté |
---|---|---|---|---|
Table de hachage (ne pas utiliser en production) | org.hibernate.cache.HashtableCacheProvider | mémoire | yes | |
EHCache | org.hibernate.cache.EhCacheProvider | mémoire, disque | yes | |
OSCache | org.hibernate.cache.OSCacheProvider | mémoire, disque | yes | |
SwarmCache | org.hibernate.cache.SwarmCacheProvider | en cluster (multicast ip) | oui (invalidation de cluster) | |
JBoss Cache 1.x | org.hibernate.cache.TreeCacheProvider | en cluster (multicast ip), transactionnel | oui (réplication) | oui (horloge sync. nécessaire) |
JBoss Cache 2 | org.hibernate.cache.jbc.JBossCacheRegionFactory | en cluster (multicast ip), transactionnel | oui (replication ou invalidation) | oui (horloge sync. nécessaire) |
L'élément <cache>
d'une classe ou d'une collection a la forme suivante :
<cache usage="transactional|read-write|nonstrict-read-write|read-only" region="RegionName" include="all|non-lazy" />
| |
| |
|
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.
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
>
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 la propriété hibernate.transaction.manager_lookup_class
, fournissant une stratégie pour obtenir le JTA TransactionManager
. 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 verrouillage, 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
>
Si l'application a besoin de mettre à jour les données de manière occasionnelle (il est très peu probable que deux transactions essaient de mettre à jour le même élément simultanément) et si 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()
.
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.
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 20.2. Support de stratégie de concurrence du cache
Cache | read-only (lecture seule) | nonstrict-read-write (lecture-écriture non stricte) | read-write (lecture-écriture) | transactional (transactionnel) |
---|---|---|---|---|
Table de hachage (ne pas utiliser en production) | yes | yes | yes | |
EHCache | yes | yes | yes | |
OSCache | yes | yes | yes | |
SwarmCache | yes | yes | ||
JBoss Cache 1.x | yes | yes | ||
JBoss Cache 2 | yes | yes |
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()
ou 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 premier niveau de la session.
ScrollableResult cats = sess.createQuery("from Cat as cat").scroll(); //a huge result set
while ( cats.next() ) {
Cat cat = (Cat) cats.get(0);
doSomethingWithACat(cat);
sess.evict(cat);
}
La Session
fournit également une 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 du cache d'une instance, de la classe entière, d'une instance de collection ou du rôle entier d'une collection.
sessionFactory.evict(Cat.class, catId); //evict a particular Cat
sessionFactory.evict(Cat.class); //evict all Cats
sessionFactory.evictCollection("Cat.kittens", catId); //evict a particular collection of kittens
sessionFactory.evictCollection("Cat.kittens"); //evict all kitten collections
Le CacheMode
contrôle la manière dont une session particulière interagit avec le cache de second niveau :
CacheMode.NORMAL
- lit et écrit les articles dans le cache de second niveau
CacheMode.GET
- lit les articles du cache de second niveau mais ne les écrit pas sauf dans le cas d'une mise à jour des données
CacheMode.PUT
- écrit les articles dans le cache de second niveau mais ne les lit pas dans le cache de second niveau
CacheMode.REFRESH
- écrit les articles dans le cache de second niveau mais ne les lit pas dans le cache de second niveau, outrepasse l'effet de hibernate.cache.use_minimal_puts
, en forçant un rafraîchissement du cache de second niveau pour chaque article lu dans la base de données.
Pour parcourir le contenu du cache de second niveau ou la région du cache dédiée aux requêtes, vous pouvez utiliser l'API Statistics
:
Map cacheEntries = sessionFactory.getStatistics()
.getSecondLevelCacheStatistics(regionName)
.getEntries();
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 :
hibernate.generate_statistics true hibernate.cache.use_structured_entries true
Query result sets can also be cached. This is only useful for queries that are run frequently with the same parameters.
Caching of query results introduces some overhead in terms of your applications normal transactional processing. For example, if you cache results of a query against Person Hibernate will need to keep track of when those results should be invalidated because changes have been committed against Person. That, coupled with the fact that most applications simply gain no benefit from caching query results, leads Hibernate to disable caching of query results by default. To use query caching, you will first need to enable the query cache:
hibernate.cache.use_query_cache true
This setting creates two new cache regions:
org.hibernate.cache.StandardQueryCache
, holding the cached query results
org.hibernate.cache.UpdateTimestampsCache
, holding timestamps of the most recent updates to queryable tables. These are used to validate the results as they are served from the query cache.
If you configure your underlying cache implementation to use expiry or timeouts is very important that the cache timeout of the underlying cache region for the UpdateTimestampsCache be set to a higher value than the timeouts of any of the query caches. In fact, we recommend that the the UpdateTimestampsCache region not be configured for expiry at all. Note, in particular, that an LRU cache expiry policy is never appropriate.
As mentioned above, most queries do not benefit from caching or their results. So by default, individual queries are not cached even after enabling query caching. To enable results caching for a particular query, call org.hibernate.Query.setCacheable(true)
. This call allows the query to look for existing cache results or add its results to the cache when it is executed.
The query cache does not cache the state of the actual entities in the cache; it caches only identifier values and results of value type. For this reaso, the query cache should always be used in conjunction with the second-level cache for those entities expected to be cached as part of a query result cache (just as with collection caching).
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();
If you want to force the query cache to refresh one of its regions (disregard any cached results it finds there) you can use org.hibernate.Query.setCacheMode(CacheMode.REFRESH)
. In conjunction with the region you have defined for the given query, Hibernate will selectively force the results cached in that particular region to be refreshed. This is particularly useful in cases where underlying data may have been updated via a separate process and is a far more efficient alternative to bulk eviction of the region via org.hibernate.SessionFactory.evictQueries()
.
Dans les sections précédentes, nous avons couvert les collections et leurs applications. Dans cette section, nous allons explorer de nouveaux problèmes en rapport aux collections en cours d'exécution.
Hibernate définit trois types de collections de base :
les collections de valeurs
Association un-à-plusieurs
les associations plusieurs-à-plusieurs
Cette classification distingue les différentes relations entre les tables et les clés étrangères mais ne n'en dit pas suffisamment 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 lignes des collections. Cela nous amène aux classifications suivantes :
collections indexées
ensembles (sets)
sacs (bags)
Toutes les collections indexées (maps, lists, arrays) ont une clé primaire constituée des colonnes clés (<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 une ligne particulière peut être localisée efficacement lorsque Hibernate essaie de la mettre à jour ou de la supprimer.
Les ensembles ont une clé primaire composée de <key>
et des colonnes représentant l'élément. Elle est donc moins efficace pour certains types d'éléments de collection, 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-à-plusieurs ou plusieurs-à-plusieurs, en particulier lorsqu'on utilise des entités ayant des identifiants techniques, il est probable que cela soit aussi efficace (note : si vous voulez que SchemaExport
crée effectivement la clé primaire d'un <set>
pour vous, vous devez déclarer toutes les colonnes avec not-null="true"
).
Le mappage à l'aide de <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 sacs. Dans la mesure où un sac permet la duplication des valeurs d'é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 entre les lignes dupliquées. Hibernate résout ce problème en supprimant complètement (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-à-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 les lignes individuelles de la collection).
La discussion précédente montre clairement que les collections indexées et (la plupart du temps) les ensembles, permettent de réaliser le plus efficacement les opérations d'ajout, de suppression ou mise à jour d'éléments.
Les collections indexées ont un avantage sur les ensembles, dans le cadre des associations plusieurs-à-plusieurs ou de collections de valeurs. À cause de la structure inhérente d'un Set
, Hibernate n'effectue jamais de ligne UPDATE
quand un élément est modifié. Les modifications apportées à un Set
se font via un INSERT
et DELETE
(de chaque ligne). Une fois de plus, ce cas ne s'applique pas aux associations un-à-plusieurs.
Après s'être rappelé que les tableaux ne peuvent pas être chargés en différé, nous pouvons conclure que les lists, les maps et les idbags sont les types de collections (non inversées) les plus performants, avec les ensembles pas loin derrière. Les ensembles sont le type de collection le plus courant dans les applications Hibernate. Cela vient du fait que la sémantique des ensembles est la plus naturelle dans le modèle relationnel.
Cependant, dans des modèles objet bien conçus avec Hibernate, on constate que la plupart des collections sont en fait des associations un-à-plusieurs avec inverse="true"
. Pour ces associations, les mises à jour sont gérées au niveau de l'association "plusieurs-à-un" et les considérations de performance de mise à jour des collections ne s'appliquent tout simplement pas dans ces cas-là.
Avant que vous n'oubliez les sacs pour toujours, il y a un cas précis où les sacs (et les listes) sont bien plus performants que les ensembles. Pour une collection marquée comme inverse="true"
(le choix le plus courant pour un relation un-à-plusieurs bidirectionnelle), nous pouvons ajouter des éléments à un sac ou une liste sans avoir besoin de l'initialiser (charger) les éléments du sac! Ceci parce que Collection.add()
ou Collection.addAll()
doit toujours retourner vrai pour un sac 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); //no need to fetch the collection!
sess.flush();
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 appelez list.clear()
, par exemple). Dans ce cas, Hibernate fera un simple DELETE
et le travail est fait !
Supposons que nous ajoutions un élément unique 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 sac). Cela est préférable.
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 lignes une à une puis en insérer trois
effacer la totalité de la collection (en un SQL DELETE
) puis insérer les cinq éléments restant (un à un)
Hibernate n'est pas assez intelligent pour savoir que, dans ce cas, la seconde option est plus rapide (Il vaut mieux que 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 (c'est-à-dire la deuxième stratégie) à tout moment en libérant (c'est-à-dire en déréférençant) la collection initiale et en retournant une collection nouvellement instanciée avec tous les éléments restants.
Bien sûr, la suppression en un coup ne s'applique pas pour les collections qui sont mappées avec inverse="true"
.
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
.
Vous pouvez accéder aux 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 fabrique. Voici un code qui montre un exemple de configuration minimaliste :
// MBean service registration for a specific SessionFactory
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "myFinancialApp");
ObjectName on = new ObjectName("hibernate", tb); // MBean object name
StatisticsService stats = new StatisticsService(); // MBean implementation
stats.setSessionFactory(sessionFactory); // Bind the stats to a SessionFactory
server.registerMBean(stats, on); // Register the Mbean on the server
// MBean service registration for all SessionFactory's
Hashtable tb = new Hashtable();
tb.put("type", "statistics");
tb.put("sessionFactory", "all");
ObjectName on = new ObjectName("hibernate", tb); // MBean object name
StatisticsService stats = new StatisticsService(); // MBean implementation
server.registerMBean(stats, on); // Register the MBean on the server
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()
.
Hibernate fournit plusieurs métriques, qui vont des informations très basiques aux informations très spécialisées qui ne sont appropriées que dans certains scénarios. 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 aka).
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 les hit, miss du 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 (c'est-à-dire, 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
, et QueryStatistics
pour plus d'informations. Le code ci-dessous montre un exemple simple :
Statistics stats = HibernateUtil.sessionFactory.getStatistics();
double queryCacheHitCount = stats.getQueryCacheHitCount();
double queryCacheMissCount = stats.getQueryCacheMissCount();
double queryCacheHitRatio =
queryCacheHitCount / (queryCacheHitCount + queryCacheMissCount);
log.info("Query Hit ratio:" + queryCacheHitRatio);
EntityStatistics entityStats =
stats.getEntityStatistics( Cat.class.getName() );
long changes =
entityStats.getInsertCount()
+ entityStats.getUpdateCount()
+ entityStats.getDeleteCount();
log.info(Cat.class.getName() + " changed " + changes + "times" );
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 suivantes : getQueries()
, getEntityNames()
, getCollectionRoleNames()
, et getSecondLevelCacheRegionNames()
.
Copyright © 2004 Red Hat, Inc.