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.
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 :
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!
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.
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 :
<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 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 :
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 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.
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 :
<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 d'==
des proxy.
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 proxys différents, l'instance de base 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 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="CatImpl" proxy="Cat"> ...... <subclass name="DomesticCatImpl" proxy="DomesticCat"> ..... </subclass> </class>
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).
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();
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.
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 :
( (Integer) s.createFilter( collection, "select count(*)" ).list().get(0) ).intValue()
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.
s.createFilter( lazyCollection, "").setFirstResult(0).setMaxResults(10).list();
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
:
<class name="Person" batch-size="10">...</class>
Hibernate exécutera désormais trois requêtes, en chargeant respectivement 10, 10, et 5 entités.
You may also enable batch fetching of collections. For example, if each Person
has a lazy collection of Cat
s, and 10 persons are currently loaded in the Session
, iterating through all persons will generate 10 SELECT
s, one for every call to getCats()
. If you enable batch fetching for the cats
collection in the mapping of Person
, Hibernate can pre-fetch collections:
<class name="Person"> <set name="cats" batch-size="3"> ... </set> </class>
Avec une taille de lot (batch-size
) de 3, Hibernate chargera respectivement 3, 3, 3, et 1 collections 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 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.)
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.
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 :
<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 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 :
<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>
A different (better?) way to avoid unnecessary column reads, at least for read-only transactions is to use the projection features of HQL or Criteria queries. This avoids the need for buildtime bytecode processing and is certainly a preferred solution.
Vous pouvez forcer le mode de chargement agressif des propriétés en utilisant fetch all properties
dans les requêts HQL.