Hibernate 4.3 nous a donné pas mal de fil à retordre, donc on va vous en parler un peu. Juste des idées de base. On espère avancer sur chaque sujet dans la suite, mais là, l'idée c'est de mettre en place les concepts.

Les concepts d'Hibernate

Alors, la problématique est simple: on a des objets à l'exécution, en mémoire, et une base de données relationnelle, qui survivra à l'arrêt de la JVM. On veut lire, écrire des données d'une source à l'autre. Hibernate est un ORM de JBoss.

Configuration:

la configuration d'Hibernate se fait en deux fois:

  1. La configuration du lien entre classes et tables, qu'on appelle mapping. Celle ci répond aux questions comme "j'ai une instance de A, je la mets dans quelle table, et où je mets mes attributs?" ou "ok, comment je fabrique une instance de ma classe A depuis ma base?"
  2. La configuration de la base de données: accès, propriétés générales des connexions, et amorcer la configuration du mapping en donnant leur adresse. Ce sont des propriétés générales qui ne dépendent pas du mapping.

Dans le premier cas, la configuration est en fait basée sur le pattern Metadata Mapping, qui permet de faire le lien par introspection entre les attributs de l'objet et la structure de la table. Voir ici (merci Fowler)

Le pattern Unit of Work

Avant de se lancer dans les méandres de l'outil de JBoss, on va prendre un peu de hauteur. Une partie du problème est qu'on va charger des données qui vont vivre dans l'application, puis on va vouloir répercuter les éventuels changements en base. Le but du pattern est de gérer la vie des objets par rapport à une source de données, disons, au hasard, une base de données. Par exemple, on charge une personne, on met à jour son adresse, et on la persiste. Comme Fowler nous le dit très bien ici, Unit of Work nous donne une méthode pour nous y retrouver.

Source: martinfowler.com

Le pattern Unit of work selon Fowler

Il nous dit qu'il suffit de conserver l'état de l'objet vis à vis de la base. L'état est:

  1. new: l'objet est à créer en base (ordre insert)
  2. delete: l'objet est à supprimer en base (ordre delete)
  3. l'objet est clean (propre): pas d'écart entre la version base et la version objet en mémoire. Dans ce cas là, il n'y a rien à faire
  4. l'objet est dirty (sale): il y a un écart entre la base et la mémoire: il va falloir faire un update

Quand on va exécuter un commit, tous les ordres SQL vont être générés puis exécutés. A la fin de l'exécution, si tout se passe bien, tous les écarts entre la base et la mémoire sont résolus. Quant au rollback, comme son nom l'indique... On vide les listes et on laisse l'utilisateur se débrouiller.

Lazy loader

Une idée simple: quand on veut le nom d'une personne, on n'a pas besoin de son dossier dentaire. Basé sur un constat simple, le lazy loader ne charge que les informations dont il a besoin.  Si A est une classe qui contient l'attribut a et une liste de B, alors, a priori, le chargement de A veut dire que a l'est aussi, mais pas la liste de B. Quand on accède à la liste, et seulement à ce moment là, la liste est chargée. Ce qui a plusieurs conséquences non triviales:

  • La liste est chargée à la demande, ce qui n'est pas le comportement par défaut. Hibernate décore le code, ou en tous cas modifie le comportement par défaut. Amusez vous à mettre une classe finale et vous le constateriez
  • La session doit être ouverte quand on demande le premier accès à la liste.

Et blam, un exemple d'après Fowler:

source: martinfowler.com

L'idée d'Unit of Work d'après Fowler

Caches:

La notion de cache est assez basique, et on a en général que le niveau N+1 contient plus de données que celui de niveau N, mais ce dernier est plus rapide. Dans le cas d'Hibernate, il existe deux caches, et celui de second niveau est optionnel. En fait:

  1. le premier cache est géré au niveau d'une transaction au sein d'une session. Il est vidé dès la fin d'une transaction, et les objets ne sont visibles que dans la transaction. Voir à ce sujet UoW.
  2. Le second est géré au niveau de la JVM et son usage doit être explicité dans le mapping. Le cache ne dépend donc ni de la session, ni de la transaction. On peut gérer ce cache suivant différentes méthodes (read only, etc). Un usage possible est d'avoir en mémoire d'accès plus rapide des objets qui ne vont pas être modifiés, plutôt qu'en base. On peut mettre dans celui ci des collections et des entités.

Hibernate 4: comment ça marche?

A présent, on connait un peu mieux les idées de base, voici comment Hibernate les met en place.

Classes importantes:

On ne va pas toutes les faire, mais si vous en avez seulement 5 à retenir, ce sont celles ci:

  • Configuration: la configuration lit les propriétés de la base de données (pour créer des sessions) et le mapping des classes. On peut ajouter les classes à la main, cela dit. Une fois que le mapping et la configuration de la base sont chargés, on peut fabriquer une session factory. En fait, avec Hibernate 4.3.5, la sessionFactory est fabriquée depuis cette classe avec un service registry. Un petit google vous en dira plus.
  • SessionFactory: une session factory est l'objet qui maintient le cache de second niveau, et elle permet de fabriquer des sessions. Elle contient également les méta données et offre un accès au cache. Elle permet enfin de récupérer la session courante.
  • Session: Attention: contrairement à ce que le nom peut laisser présager, une session est attachée à une transaction. Elle est ouverte, la transaction est ouverte, on exécute, on commit (si tout va bien), on ferme la transaction, on ferme la session. Pour citer la doc: The lifecycle of a Session is bounded by the beginning and end of a logical transaction. Elle permet également la fabrication de requêtes. Nous reviendrons plus tard sur cette classe.
  • Transaction: une unit of work (suivant le pattern ci dessus) (vous avez vu comment cet article est bien pensé) (extraordinaire, hein?) associée à une session. Alors, ça peut surprendre, mais en fait, vous allez appliquer le pattern sur la session, mais valider la transaction via la classe Transaction. La session vous permet d'obtenir une instance de transaction.

 Donc, à ce stade, on sait ouvrir une session, on peut ouvrir et fermer une transaction. C'est bien. La classe Query (qui implémente le pattern du même nom) permet le requêtage. Et en voici (toujours selon Fowler) le principe:

source: martinfowler.com

Le pattern Query (selon Fowler)

Voici en gros ce qu'il faut savoir sur les classes. Par contre, on va rentrer dans le vif du sujet concernant la mise en place d'unit of work en Hibernate.

Comment gérer vos sessions et transactions?

Alors, en fait, tout est dit ici, mais on va y revenir.

Cycle de vie des objets:

Hibernate, dans la vie d'une application, va gérer vos objets suivant trois états:

  1. Vous créez une instance de Personne (en supposant qu'on a le mapping qui va bien). Elle n'est pas sauvée en base, vous voulez la sauver. Cas 1: l'objet n'est pas en base mais va y aller. Aucune session ne le contient, il est dit transcient.
  2. Vous ouvrez une session, l'objet est sauvé en base, et la session contient l'objet. Il est dit persistant. Quand on veut répercuter le changement en base, l'objet est lié à la session, et c'est simple. C'est aussi le cas quand vous chargez l'objet depuis la base. Par définition, tant que la session n'est pas fermée, il est lié à elle. Cas 2: l'objet est persistant. Alors, à ce stade, vous et votre objet vivez dans un monde merveilleux: vous avez accès aux champs quand vous les demandez.
  3. Vous avez chargé un objet, vous avez fermé la session, et l'objet n'est plus relié à cette session. Vous ne pouvez plus le sauver (en utilisant cette session) ni accéder aux champs qui ne sont pas déjà chargés. L'objet est dit détaché. Sous entendu: de sa session.

Alors, ça a l'air un peu abstrait comme ça, mais en fait, c'est très simple: si vous voulez vivre sans exception brutale sur un get, réflechissez un peu. Par exemple: vous chargez un pays, la France, qui contient une liste d'habitants (une modélisation discutable), vous aurez un cas qui se passe bien et pas l'autre:

  1. Ouverture de la session, recherche du pays "France", accès aux habitants, fermeture de la session. Tout va bien, vous faites un getPeople pendant que l'objet est attaché à la session.
  2. Ouverture de la session, recherche du pays "France", fermeture de la session, accès aux habitants qui lève une exception. L'objet est détaché, la liste des habitants est chargée (par lazy load) après, ce qui nécessite un accès à la base qu'on n'a plus. Exception, fin du monde.

Donc, ça amène son lot de difficultés dans l'architecture de votre application.

Manier Hibernate et bien gérer ses objets:

Scénario classique sur n'importe quel client web:

  1. Un utilisateur se connecte, demande le chargement d'une page qui nécessite un accès base sur les instances d'une classe A. L'utilisateur charge les instances de A et les affiche. Coté serveur, ouverture d'une session, accès base, fermeture d'une session.
  2. L'utilisateur choisit une instance de A et demande à charger les informations sur celle ci. Ouverture d'une session, recherche de l'élément correspondant, trouver l'élément, charger ses données, fermeture de la session.
  3. L'utilisateur modifie quelques champs sur A (ou une instance liée) et sauve la nouvelle instance en base. Ouverture d'une session, recherche de l'élément correspondant, trouver l'élément, charger ses données, mettre à jour les champs sur l'instance, commiter, fermeture de la session.
  4. L'utilisateur ferme sa session.

Et là, on voit qu'en fait, une manipulation aussi élémentaire nécessite plusieurs ouvertures de session, avec la gestion d'objets détachés. Il y a plusieurs solutions:

  • Pour gérer les fermetures de session avant la fin des accès (comme fermer une session puis faire un get), le pattern DTO a son charme puisqu'il vous force à construire l'objet complet avant de l'envoyer à qui en a besoin.
  • Pour gérer la question de rattacher un objet à une session, on a connu deux écoles. Celle qui dit: ma classe métier contient un champ nullable Id, qui est "un id technique" (en fait l'id en base) ou celle qui opte pour les clés fonctionnelles. Dans le premier cas, votre objet porte son id dans toutes les couches, ce qui permet de charger par id son équivalent attaché et de faire du copier coller d'attributs. Les puristes qui vous disent que c'est "caca" et qui trouvent un objet à partir de critères d'unicité (ex: France comme nom de Pays, numéro de sécu pour une personne, etc). Voilà.
  • Les gens qui laissent Hibernate gérer la session, et qui vous font un getCurrentSession quand ils en ont besoin. Ni open, ni close, juste un get. Pas de retour catégorique à émettre de notre coté...

A vous de voir!

Les bases de données épaisses

Dernière partie d'un article qui promet déjà d'être long: les bases de données épaisses nous ont été présentées par monsieur Rudy Bruchez, qui nous a donné cette URL: ici (vas y, clique). Donc, le constat est double:

  1. les personnes qui développent prennent un ORM pour un outil magique, mais la performance est minable. Trop de requêtes générées
  2. ces mêmes personnes sont en fait opposées à une saine utilisation des bases de données, qu'elles assimilent à un support de stockage au même titre qu'un disque dur, le langage de requêtage en plus.

Et donc, NON, il faut considérer une base comme un service extérieur, avec une API à lui, et qui doit être accédé via cette API.

Conclusion

Il y a des millions de choses à dire sur Hibernate, et là, nous venons à peine d'effleurer les concepts. On reprendra une série d'articles pour aller dans le coeur du framework. En attendant, vous avez de quoi comprendre ce que c'est (ou n'est pas). Nous n'avons pas traité le mapping, déjà parce que c'est long, et qu'ensuite, c'est un sujet en soi. Oui, c'est facile de remettre au surlendemain ce qui peut être fait demain, mais... C'est comme ça!

 

Comments are closed.

Set your Twitter account name in your settings to use the TwitterBar Section.