Chapitre 19. Améliorer les performances

19.1. 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.

19.1.1. 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 :

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.

19.1.2. 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 :

<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.

19.1.3. 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 :

<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>

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.

19.1.4. 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 :

( (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();

19.1.5. 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 :

<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 Cats, and 10 persons are currently loaded in the Session, iterating through all persons will generate 10 SELECTs, 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 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.)

19.1.6. 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.

19.1.7. 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 :

<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.