Table des matières

Cet article sert de mémo pour l'utilisation d'hibernate, le plus connu des ORM (Object Relational Mapping) java.

Logging

Liste de package SQL et hibernate

 log4j.logger.org.hibernate       --> tout hibernate
 log4j.logger.org.hibernate.type  --> valeur des paramètres de requêtes hql ou criteria
 log4j.logger.org.hibernate.cache --> action de cache de 1er et 2nd niveau
 log4j.logger.org.hibernate.SQL   --> visualiser le SQL généré/exécuté
 log4j.logger.org.hibernate.hql   --> affiche le résultat traduction des requetes HQL en SQL (AST)

Activer la trace des classes de certains package

Methode1 : dans le code directement

> Avec LOG4J

 org.apache.log4j.Logger.getLogger("org.hibernate").setLevel(Level.ERROR);
 org.apache.log4j.Logger.getLogger("org.hibernate.SQL").setLevel(Level.DEBUG);

> Avec SLF4J

 LoggerFactory.getLogger("org.hibernate").isDebugEnabled(null);
Methode2 : dans le fichiers de conf log4j.properties

Exemple de contenu du fichier log4j.properties

<sxh ini> .

 log4j.logger.org.hibernate=debug, stdout
 log4j.logger.org.hibernate.SQL=debug, stdout
 log4j.logger.org.hibernate.cache=debug, stdout
 log4j.logger.org.hibernate.type=debug, stdout
 log4j.additivity.org.hibernate=false
 log4j.additivity.org.hibernate.cache=false
 log4j.additivity.org.hibernate.type=false
 log4j.additivity.org.hibernate.SQL=false

</sxh>

Remarque sur log4j : additivity On positionne à false (tel que montré ci-dessus), les packages pour lesquels on ne souhaite pas que les messages tracés par les sous packages soient affichés 2 fois. Ca permet d'éviter la propagation des messages de log au logger parent des packages spécifiés.

Par exemple ici, les classes de org.hibernate.SQL loggeront sur la sortie standard tous messages dont le niveau est >= debug.

Mais comme on a spécifié ceci : log4j.logger.org.hibernate=debug, stdout, les appender (en l'occurrence stdout) du logger org.hibernate sera appelé. –> messages en double

Avec addivity, on évite cela

Methode2 : dans le fichiers de conf application-<ENV>.yml
 spring:
     profiles:
         active: dev
 ...
 # ajout de config slf4j ou log4j pour augmenter le niveau de log de certains package, ici liquibase notamment
 logging:
     level:
        org.hibernate.tool: DEBUG
 ...

Fichier de conf Hibernate

 jdbc.driver=UneClasseDeDriverAUneDataBase
 sql.dialect=DialectSQL
 hbm2ddl.auto=(create or validate or none)
 hibernate.format_sql=false
 hibernate.use_sql_comments=false
 ...

Remarque : Il y a plein d'autres paramètres, mais les plus importants sont :

 jdbc.driver
 hibernate.show_sql : enable query logging
 hibernate.format_sql : pretty prints SQL
 hibernate.use_sql_comments : adds an explanatory comment
 hbm2ddl.auto --> (update or validate or none)

# Application.properties Jpa commentés a true –> permet d activer les traces hql

Fichier de conf YML avec utilisation de jpa

<sxh conf>

 spring:
     ...
     datasource:
         type: com.zaxxer.hikari.HikariDataSource
         url: jdbc:mysql://localhost:3306/testify_jhi?useUnicode=true&characterEncoding=utf8&useSSL=false
         name:
         username: root
         password: root
         ... etc.
     jpa:
         database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
         database: MYSQL
         show-sql: true
         properties:
             hibernate.cache.use_second_level_cache: true
         hibernate:
             ddl-auto: validate # ddl-auto is a JPA properties, which is translated by JPA when configuring Hibernate by setting hbm2ddl.auto
             naming:
                 strategy: org.springframework.boot.orm.jpa.hibernate.SpringNamingStrategy               
             ... 

</sxh>

Utilisation

Définitions des notions rencontrées

Session, connection SQL et transaction

Session La plupart des base oracle rencontrées dans le monde professionnel sont paramétrées par les DBA pour n'avoir qu'une seule session par connection ; ainsi quand on parle de connection, il n'y a pas d'ambiguité.

Néanmoins, dans les bases ORACLE et peut être d'autres, on peut paramétrer le SGBD pour qu'une connection puisse contenir plusieurs sessions. Ces sessions sont alors concurrentes : les modifications effectuées dans une session s1 de la connection c1 n'est pas visible depuis la session s2 de la même connection c1 ou d'autre connection. A moins biensur qu'une instruction de commit depuis la session aie eu lieu sur les tables concernées.

Pour Hibernate, on retrouve cette notion de Session, telle que définie au dessus : org.hibernate.Session

Transaction Une transaction = suite séquentielle d'instructions SQL. Une transaction appartient à une session. Elle est atomique. La transaction est entièrement jouée, ou pas du tout, notamment si un problème intervient, ou si un ordre de rollback a été demandé.

Ainsi, une session ne peut avoir qu'une seule transaction active. Les autres transactions ont forcément été fermées (en commit ou rollback).

  session s1 : s1t1 -> s1t2 -> s1t3 -> ... **s1t9**
  session s2 : s2t1 -> **s2t2**

La transaction courante de la session s1 est la s1t9 ; celle de s2 est la s2t2.

Il est possible d'ouvrir ou fermer une transaction ; avec l'API hibernate par exemple :

 Session.beginTransaction()
 Session.endTransaction()

Entité managée, entité détachée

En utilisant hibernate pour récupérer des entités, on peut directement récupérer des Entity. Ces Entity sont managées, i.e elles sont attachées à la session qui les a récupérées. Cela permet à hibernate de lancer d'autre requête pour récupérer des attributs qui n'ont pas encore été récupérés sur l'Entity. Au final, il faut comprendre l'état “managé” comme “en lien avec la base de donnée) : on peut encore consulter ou modifier d'autres propriétés sur cette Entity en utilisant ses getters et setters.

Dans le cas où on a modifié l'entité, il sera nécessaire de faire un session.saveOrUpdate(entiteeModifiee) ou un entityManager.persist(entiteeModifiee).

Ces entités restent à l'état managée tant que :

Par exemple :

MonEntite1
  @Id
  Long       id;
  String     a;
  MonEntite2 entite2;
  getters/setters

puis

 MonEntite1 m = sessions.createQuery("from MonEntite1").list().get(0);
 MonEntite2 = m.getEntite2();

MonEntite2 est par défaut en LAZY. Cela implique qu'elle n'est pas récupérée lors de la première requête. Elle le sera au moment où m.getEntite2() est invoqué.

Cette notion d'entity managée peut être dangereuse, parce qu'on oublie souvent que l'entity n'est pas un Dto.

Dans quel cas est-ce dangereux ?

Dans ces 2 cas, on aura une exception hibernate pas très facile à comprendre …

Pour voir en deboggage si une entité récupérée est managée ou non : Dans un premier temps, il faut :

  1. sélectionner l'onglet expression
  2. cliquer sur le bouton chevron dirigé vers le bas (à coté du redimensionnement de l'onglet, puis sélectionner java.
  3. S'assurer alors que l'item “Show References” est bien coché (en cliquant dessus si nécessaire).

Ensuite, mettre un point d'arrêt manipulant une instance de l'entité en question, puis mettre la variable dans l'onglet expression, et faire un expand sur referenced from (voir image ci-dessous). Normalement vous devriez voir dans le bloc d'affichage de la valeur de l'expression sélectionnée le mot MANAGED.

Cache de premier et de second niveau et cache de requete

Cache de premier niveau
Cache de second niveau

on stocke des sortes de hashmap @Id –> Objet

Remarque : le cache de second niveau ne met pas par défaut en cache les champ collection des entité cacheable. Pour ce faire il faut aussi explicitement annoter @cacheable. 
 
Cache de requete

Si la requete est eactement la même, les résultats de la requete précédente ayant été mis en cache, la réponse est immédiate.

JPA : Java Persistence API

JPA est un ensemble de classes, interface, annotations qui permettent en fait de se détacher d'hibernate. Si demain, on décide d'utiliser un autre ORM qu'Hibernate et que toutes les manipulations d'objets ont été faites à partir de l'API JPA, alors ce sera bien plus simple.

Aux classes hibernate Session et SessionFactory correspondent notammenent le classes EntityManager et EntityManagerFactory.

En fait, une instance de EntityManager est un wrapper de Session : il contiendra une session hibernate. On peut retrouver cette Session hibernate avec le code suivant :

  Session session = entityManager.unwrap(Session.class);

Spring Data

Fournit des interfaces et implémentations automatisées suivantes :

Session.saveOrUpdate(entity) ou EntityManager.persist(entity) (JPA)

session.merge()

session.flush()

Les entitées qui ont été modifiées par le code ne le sont pas forcément dans la session (connection) de la base. Flush permet d'exécuter les instructions SQL relatives à ces changements.

Les instructions SQL exécutée par un flush modifie la session (connection) à la base de l'utilisateur ; ces modifications ne sont pas encore commitées. Ca revient à faire un INSERT INTO … VALUES (…), mais sans COMMIT derrière.

Si le flush() n'est pas encore fait, un select en SQL (avec la meme session biensur) ne fournira pas les dernières modifications sur les Entity managées de la session.

⇒ Pour tous les autres utilisateurs, rien n'est encore visible. Ces données deviendront visibles à partir du moment où un COMMIT est effectué, i.e bien souvent à la fermeture de la session, ou, par exemple, quand on sort d'une méthode annotée avec l'annotation JPA @Transactionnal (voir plus bas)

session.clear()

Efface le contexte de persistence : =⇒ toutes les instances Entity managée de la session sont détachés et, plus important encore : Les éventuelles modifications qui avait été faites sur certaines entités sont elles aussi perdues à jamais !!!

=⇒ Il est donc souvent préférable de faire un session.flush() avant !!!!

session.beginTransaction()

La session est intimement lié à la transaction courante de la session. Si bien qu'en fait, les méthodes commit et rollback se trouvent plutôt dans la classe Transaction. Le schéma type de l'utilisation d'une session est la suivante :

<sxh java>

 Session sess = factory.openSession();
 Transaction tx = null;
 try {
     tx = sess.beginTransaction();
     // do some work
     ...
     tx.commit();
 }
 catch (RuntimeException e) {
     if (tx != null) tx.rollback();
     throw e; // or display error message
 }
 finally {
     sess.close();
 }

</sxh>

Il est normalement possible de réaliser plusieurs transaction dans une même session de la manière suivante :

<sxh java>

 Transaction transaction;
 transaction = session.beginTransaction();
 //... (operations in the context of transaction)
 transaction.commit();
 //... (other commands outside of any transaction)
 transaction = session.beginTransaction();
 //... (and so on and so forth ...)

</sxh>

Hibernate.isInitialized(proxy)

Très utile dans les tests de DAO avec des left join fetch. Pour vérifier que les entitées root ont bien été chargées avec certaines de leurs autres champs dont le type se sert d'une autre entité.

<sxh java> Hibernate.isInitialized(monEntite1.monEntite2) ou Hibernate.isInitialized(monEntite1.listeDesEntite2) </sxh> === @Transactionnal (Annotation JPA) === Cette annotation indique que juste avant l'exécution de la méthode, le moteur JPA doit avoir réussi à récupérer depuis l'entity manager une session hibernate (ou autre ORM) valide, et capable de gérer les transactions (certaines sessions hibernate ne le peuvent pas). Au final, @Transactional signifie : démerde toi pour me fournir une transaction, en ouvrant une nouvelle session hibernate. (Rqe : Je ne parlerai pas du rapport entre le session et les connection SQL parce que je connais très peu, mais j'indique qu'il quasi certain que les sessions sont déjà ouvertes, donc les connections aussi, il suffit dans le constater dans SQL developper en consultant les sessions SQL ouvertes par l'application). == @Transactional sans paramètre d'annotation == A partir du moment où on est dans le corps d'une méthode A.m1 annotée @Transactional, une session est affectée et une transaction de cette session est active. Si cette méthode appelle un autre méthode B.m2 annotée @Transactional sans paramètre d'annotation, alors la transaction est passée à B.m2() et ainsi de suite. La transaction sera fermée à la fin de la méthode @Transactional racine, i.e A.m1(), en rollback ou commit. La session sera elle aussi fermée, pour être utilisée par d'autres méthodes en parallèle (Threads). == @Transactional(Propagation.REQUIRES_NEW) == On peut utiliser le paramètre Propagation.REQUIRES_NEW dans certains cas, mais (il me semble que) la condition pour que ce paramètre soit bien pris en compte est que la méthode m2() soit dans une autre classe. A.m1() –> B.m2() Dans ce cas : * A.m1() se voit attribuée une session s1 et une transaction s1t1. * Quand elle appelle B.m2(), une nouvelle session s2 est prise pour la fournir à B.m2() * B.m2() fait sa tambouille et se termine (en rollback ou commit) ; s2t1 et s2 sont fermée. * A.m1() continue alors toujours avec sa session s1. Ce mécanisme peut parfois s'avérer utile, notamment quand B.m2 travaille a besoin de travailler en concurrence (multithread) sur un objet en base communs à tous les threads, on met du coup B.m2 en synchronized. === Requêtes hibernate === == Récupération entité managée == Au sein d'une session, les entitées récupérées à partir des méthodes de l'instance session (par requête ou autre) sont managées, elle peuvent donc servir à mettre à jour la base de donnée. Par contre, si on utilise les classes proposées par JPA data (JpaRepository, voir plus bas), il me semble que les entitées récupérées sont détachées, excepté pour les méthode getOne() == entité root, et requêtes embarquées basées sur l'entitée root (aliasée) == En HQL, il n'est pas possible de faire de sous requête de type select … where a in (). Pour parer à ce problème, on considère toujours une entité racine, la rootEntity, qu'on alias ; si on a besoin de récupérer d'autres informations, on les lie à la clause select de la rootEntity en faisait des sousRequete. Exemple : rootEntity EntiteA <sxh sql> select ea.a, ea.ref1, ea.c, ea.d from EntiteA ea </sxh> <sxh sql> select ea.a, (select eb.b from EntityB eb where eb.id=ea.ref1) as ebb from EntiteA where ea.c=5 and ebb=17 </sxh> Remarque : il parait judicieux de rajouter un distinct dans ce genre de jointure, car on ne connait pas la cardinalité des relations obtenues. == left join et left join fetch == Dans tous 2 cas, left join opère sur une relation de l'entité amont : <sxh sql> from A left join fetch A.champRelationVersB </sxh> champRelation1 est un champ de A qui décrit une relation OneToOne ou OneToMany ou ManyToOne avec l'autre entité à tirer (i.e B) * left join : permet de récupérer les éléments sans les rappatrier dans le résultat, mais pour faire la requête. * left join fetch : fait la même chose mais fournira les éléments dans la requête == transformeurs de résultats en DTO (EligibiliteCatelOeuvreDao) == Il existe 2 sortes de transformation : * directement embarquées dans le HQL : on doit définir avant tout une classe de type Bean (champ getter setter) et son contructeur avec toutes ses valeur, qu'on passera dans la requête HQL: <sxh sql> select new net.codejava.hibernate.CountByField1(en.field1, count(en)) from EntityWithXmlMapping en where en.field1 is not empty group by en.field1” </sxh> <sxh java> public class CountByField1 { String f1; long count; public CountByField1(String f, long c) { f1= f; count = c; } public String getF1() {return f1;} public void setF1(String f1) {this.f1 = f1;} public long getCount() {return count;} public void setCount(long count) {this.count = count;} public String toString() { return “CountByField1[”+f1+“] = ” + count; } } </sxh> * après une requête HQL, utiliser query.setResultTransformer(), qui propose quelques transformeurs, et la possibilité de créer les siens (… implements ResultTransformer) Un exemple dans mon projet SACEM : EligibiliteCatelOeuvreDao.extractListCatelOeuvreSimpleDto ==== Paramètres ==== # Utiliser les paramètres, même les listes # attention a la fermeture de session et du lazyException Lorsque des actions (setter ou getter) sont faites sur une instance Entity managée, et que ce code de modification est fait en dehors de la session qui a permis de récupérer l'entité, alors 2 cas se produisent : - soit le code est en dehors de toute session ⇒ dans ce cas, on aura une lazyException. - soit le code est dans une autre session et là, on a une exception bizarre comme quoi il y a un problème entre l'entité et la session courante dans laquelle on tente de le manipulerl. ==== Recommandations ==== === Performances : préférer les left join fetch au eager === Pour une relations one2many –> 1 seule requête hibernate Rqe : le fetch permet de restituer les valeurs Le left permet de conserver l entité même si il n y a pas d objets enfant # trop de persiste –> cache augmente. –> session.clear() # ne pas mélanger hql et SQL dans la même transaction , le HQL et le SQL ne communiquent pas. Il faut les aider en faisant des refresh(), merge(), et surtout flush(). # le cache de requête ne stocke que les id des entité résultat # le cache de second niveau ne met pas en cache les champ list des entité cacheable. Pour ce faire il faut aussi explicitement annoter @cacheable. # hashcodem ===== Requete HQL avec jointures sur des champs de l'entité ===== ==== Ces requetes ne se servent pas du cache de premiers niveau et second niveau ==== Imaginons OeuvreSimple List<Titre> titres = new ArrayList(); @join( …) List<Titre> getTitres(); from OeuvreSimple os left join fetch os.titres where os.genre='MT' Ne sera pas traduit par hibernate comme devant aller chercher les titres à partir du cache de 1er ou 2nd niveau Une requête hql est directement traduite en SQL puis exécutée ==== left join fetch rootEntity.f1 f1 left join fetch f1.f2 left join fetch fn.fn+1 à proscrire ==== La traduction de left join fetch en SQL et une jointure interne. Imaginons que les cardinalités moyenne pour un élément fi sont : Ni Le résultat de la requete SQL fera alors : N1*N2*…*Nn+1 résultats. Il devra aggréger ces résultats pour construire le résultat en objet, celui à retourner. –> C'est très volumineux, et ça risque de dépasser l'espace dans le tablespace temporaire en base –> C'est très couteux en temps Il est préférable de séparer en au moins 2 sous requêtes, puis de faire algorithmiquement la jointure entre les objets récupérés