13. Dynamique des objets persistants

Jusqu’à présent nous avons travaillé sur une base de données pré-existante, et nous n’avons donc effectué aucune mise à jour avec JPA/Hibernate. En soi, il n’y a rien de beaucoup plus compliqué que pour les recherches: étant donné un objet persistant, on modifie une de ses propriétés avec un des « setteurs ». La session Hibernate, qui surveille cet objet placé dans son cache, va alors le marquer comme étant à modifier, et engendrera la requête update au moment approprié.

La problématique abordée dans ce chapitre est un peu plus générale et aborde le cycle de vie des objets de notre application. Un même objet peut en fait changer de statut. Le statut « persistant » indique une synchronisation avec la base de données pour préserver les modifications de l’état de l’objet. Mais cet objet peut également être (ou devenir) transient ou détaché. À quel moment un objet devient-il persistant? Quand cesse-t-il de l’être? Comment Hibernate détecte-t-il l’apparition d’un objet persistant et gère-t-il les mises à jour? La première session considère ces problématiques de mises à jour affectant un seul objet, et introduit la notion de transaction dans JPA/Hibernate.

Ces questions peuvent devenir relativement complexes quand on ne considère plus un seul objet, mais un graphe dont le comportement doit obéir à une certaine cohérence. Il faut alors définir des comportements répercutant, « en cascade », les mises à jour d’un objet pour qu’elles affectent également les objets dépendants et connexes dans le graphe. Ce sujet est traité dans la seconde session.

S1: Objets transients, persistants, et détachés

Supports complémentaires :

Commençons par la notion de base, celle de transaction. Rappelons qu’une transaction est une séquence d’accès (lectures ou mises à jour) à la base de données qui satisfait 4 propriétés, souvent résumées par l’acronyme ACID.

  • A comme Atomicité: les accès d’une même transaction forment un tout solidaire; ils sont validés ensemble ou annulés ensemble.
  • C comme cohérence: une transaction doit permettre de passer d’un état cohérent de la base à un autre état cohérent.
  • I comme isolation: une transaction est totalement isolée des transactions concurrentes qui s’exécutent en même temps.
  • D comme durabilité: avant la validation (commit), les mises à jour d’une transaction sont invisibles par toute autre transaction, après, ils deviennent visibles et définitifs.

Ce sont des notions de base que je vous invite à réviser si elles ne sont pas claires. Hibernate fournit une interface Transaction qui encapsule la communication avec deux types de systèmes transactionnels: les transactions JDBC, et les transactions JTA (Java Transaction API). Le second type de système est réservé à des applications complexes ayant besoin de recourir à un gestionnaire de transactions réparties. Dans notre cas nous ne considérons que les transactions JDBC.

Hibernate fonctionne toujours en mode autocommit=off, ce qui est recommandé pour éviter de valider aveuglément toutes les requêtes. D’une manière générale, toute série de requêtes / mises à jour effectuée avec Hibernate devrait être intégrée à une transaction bien identifiée.

L’interface Transaction

L’interface Transaction est donc en charge d’interagir avec la base pour initier, soumettre et valider des transactions. Cette interface peut lever des exceptions qui doivent être soigneusement traitées pour éviter de se retrouver dans des états incohérents après l’éventuel échec d’une opération. La pattern standard de gestion des exceptions est illustré ci-dessous.

// 1 - Instanciation d'un objet
        Pays pays = new Pays();
        pays.setCode("is");
        pays.setLangue("Islandais");
        pays.setNom("Islande");

// 2 - cet objet devient persistant
        Transaction tx = null;
        try {
                tx = session.beginTransaction();
                session.save(pays);
                tx.commit();
        } catch (RuntimeException e) {
                if (tx != null)
                        tx.rollback();
                throw e; // Gérer le message (log, affichage, etc.)
        } finally {
                session.close();
        }

// 3 - Ici, l'objet 'pays' est détaché de la session !

On marque donc le début d’une transaction avec beginTransaction() et la fin soit avec commit(), soit avec rollback(). Entre les deux, Hibernate charge des objets persistants dans le cache, et surveille toute modification qui leur est apportée par l’application. Un objet modifié est marqué comme dirty et sera mis à jour dans la base par une requête update (ou insert pour une création) le moment venu, c’est-à-dire:

  • à la fin de la transaction, sur un commit();
  • au moment d’un appel explicite à la méthode flush() de la session (vous ne devriez pas avoir besoin de le faire);
  • quand Hibernate estime nécessaire de synchroniser le cache avec la base de données.

Le dernier cas correspond à une situation où des données ont été modifiées dans le cache, et où une requête ramenant ces données depuis la base est soumise. Dans ce cas, Hibernate peut estimer nécessaire de synchroniser au préalable le cache avec la base (avec un flush()) pour assurer la cohérence entre le résultat de la requête et le cache. Attention: synchroniser (effectuer les requêtes de mise à jour) ne veut pas dire valider: il reste possible d’effectuer un rollback() ramenant la base à son état initial.

Le code précédent montre la gestion des exceptions. Il y a deux choses à faire impérativement:

  • annuler l’ensemble de la transaction en cours par un rollback;
  • fermer la session: une exception peut signifier que le cache n’est plus en phase avec la base de données, et toute poursuite de l’activité peut mener à des résultats imprévisibles.

La fermeture de la session implique celle de la connexion JDBC, et la suppression de tous les objets persistants situés dans le cache. La base elle-même est dans un état cohérent garanti par le rollback. Donc tout va bien.

Note

Le code montré ci-dessus est un condensé des actions à effectuer dans le cadre d’une session. Dans une application, vous devez bien entendu gérer les exceptions de manière plus systématique.

Statut des objets Hibernate

En examinant le code qui précède, on constate qu’un même objet (pays) n’est pas uniformément persistant. Il passe en fait par trois statuts successifs.

  • jusqu’au début de la transaction (et très précisément jusqu’à l’appel à save()), l’objet est transient: c’est un objet java standard géré par le garbage manager;
  • après l’appel à save(), l’objet est placé sous le contrôle de la session qui va observer (par des mécanismes d’inspection java) tous les événements qui affectent son état, et synchroniser cet état avec la base: le statut est persistant;
  • enfin, après la fermeture de la session, l’objet pays existe toujours (puisque l’application maintient une référence) mais il n’est plus synchronisé avec la base: son statut est dit détaché (sous-entendu, de la session).

On peut constater que le statut Hibernate d’un objet peut être géré de manière complètement indépendante des services fonctionnels qu’il fournit à l’application. En d’autre termes, cette dernière n’a pas à se soucier de savoir si l’objet qu’elle manipule est persistant, transient ou détaché. Pensez à l’inconvénient qu’il y aurait à devoir insérer en base un objet alors que l’on veut simplement faire appel à ses méthodes. Pensez inversement à l’intérêt de pouvoir utiliser (par exemple dans des tests unitaires) des objets sans les synchroniser avec la base de données.

Le statut d’un objet change par l’intermédiaire d’interactions avec la session Hibernate. La figure Cycle de vie des objets du point de vue JPA/Hibernate résume ces interactions et les changements de statut qu’eles entrainent.

_images/cyclevie.png

Figure 1: Cycle de vie des objets du point de vue JPA/Hibernate

Le statut « persistant » est central dans la figure. On constate qu’il existe essentiellement trois méthodes pour qu’un objet devienne persistant:

  • un objet transient, instancié avec new(), est associé à la session par save() ou saveOrUpdate(); c’est l’exemple du code ci-dessus;
  • une ligne de la base de données, sélectionnée par une requête ou une opération de navigation, est mappée en objet persistant;
  • un objet détaché est « ré-attaché » à une session.

Voici des détails sur chaque statut.

Objets persistants

Un objet persistant est une instance d’une classe mappée qui est associée à une session par l’une des méthodes mentionnées précédemment. Par définition, un objet persistant est synchronisé avec la base de données: il existe une ligne dans une table qui stocke les propriétés. la session surveille l’objet, détecte tout changement dans son état, et ces changements sont reportés automatiquement sur la ligne associée par des requêtes insert, delete ou update selon le cas. Le moment où ce report s’effectue est choisi par Hibernate. Au plus tard, c’est à l’appel du commit().

L’association à une ligne signifie qu’un objet persistant a également la propriété de disposer d’un identifiant de base de données (celui, donc, de la ligne correspondante). Le mode d’acquisition de la valeur pour cet identifiant explique les différentes méthodes disponibles.

Dans le cas le plus simple, l’identifiant est engendré par une séquence. Quand on instancie un objet avec new() et que l’on appelle la méthode save(). Hibernate va détecter que l’objet est nouvellement instancié et ne dispose par d’identifiant. Un appel insert à la base va créer la ligne correspondante, et lui affecter une valeur d’identifiant auto-générée.

Si l’identifiant n’est pas auto-généré par une séquence, il doit être fourni par l’application (c’est le cas dans notre exemple pour le pays, identifié par son code). L’exécution de l”insert risque alors d’être rejetée si une ligne avec le même identifiant existe déjà. Si, donc, on veut rendre persistant un objet dont l’identifiant est déjà connu, il est préférable d’appeler saveOrUpdate(). Hibernate déclenchera alors, selon le case un insert ou un update.

La destruction d’un objet persistant avec la méthode delete() implique d’une part son passage au statut d’objet transient, et d’autre part l’effacement de la ligne correspondante dans la base (par un delete).

Objets détachés

La différence entre un objet transient et un objet détaché est que ce dernier a été, à un moment donné, associé à une ligne de la base. On peut donc affirmer que l’objet transient dispose d’une valeur d’identifiant. On peut aussi supposer que la ligne dans la base existe toujours, mais ce n’est pas garanti puisqu’un objet détaché, du fait de la fermeture de la session, n’est plus synchronisé à la base.

Dans ces conditions, la méthode saveOrUpdate() s’impose naturellement pour les objets détachés qui sont ré-injectés dans une session. Hibernate effectue un update de la ligne existante, ou un insert si la ligne n’existe pas.

Les deux autres méthodes pour rendre persistant un objet détaché se comportent différemment.

  • la méthode update() indique à Hibernate qu’une synchronisation immédiate avec la commande SQL update doit être effectuée; c’est nécessaire quand l’objet détaché a été modifé hors de toute session, et que son état n’a donc pas été synchronisé avec la base.
  • la méthode lock() associe l’objet détaché à la session (il devient donc persistant) mais les changements d’état intervenus pendant le détachement ne sont pas reportés dans la base.

Voilà l’essentiel de ce que vous devez savoir pour comprendre et gérer individuellement le statut des objets. La prochaine session va se pencher sur le cas ou des modifications affectent des ensembles d’objets, et plus particulièrement des objets connexes dans le graphe, comme par exemple un film et l’ensemble de ses acteurs. En suivant l’approche présentée jusqu’à présent, il faudrait appeler save() ou saveOrUpdate() sur chaque objet créé ou modifié, ce qui n’est ni naturel, ni élégant.

Exercice: une transaction insérant un graphe d’objets

Créez une transaction qui insère un film, son metteur en scène et ses acteurs. Voici un bout de code ci-dessous, que vous pouvez compléter en insérant les acteurs Georges Clooney et Sandra Bullock.

Film gravity = new Film();
gravity.setTitre("Gravity");
gravity.setAnnee(2013);

Genre genre = new Genre();
genre.setCode("Science-fiction");
gravity.setGenre(genre);

// Alfonso Cuaron a réalisé Gravity
Artiste cuaron = new Artiste();
cuaron.setPrenom("Alfonso");
cuaron.setNom("Cuaron");
cuaron.addFilmsRealise(gravity);

// Ajoutez les acteurs...

// Sauvegardons dans la base
session.save(gravity);

Que se passe-t-il à l’exécution? Comment l’expliquer? Comment corriger le problème?

Hibernate permet de spécifier une propagation des commandes de persistance dans un graphe d’objet, ce qui est plus élégant et évite des erreurs potentielles. Le sujet est abordé dans la seconde session.

S2: Persistance transitive

Supports complémentaires :

Si vous avez effectué l’exercice qui précède, vous avez constaté une occurrence du problème général suivant: si je crée, avec mon application, un graphe d’objet connecté, les instructions rendant ces objets persistants doivent préserver la cohérence de ce graphe. En d’autres termes, on ne peut pas rendre certains objets persistants et d’autres transients sous peine d’aboutir à une version incohérente de la base.

Un des rôles de la transaction est d’assurer la cohérence (transactionnelle) des commandes de mise à jour. C’est le « C » dans le fameux acronyme ACID. Assurer cette cohérence « manuellement » en gérant les situations au cas par cas est source d’erreur et de programmation répétitive. Avec JPA/Hibernate, on peut introduire dans le modèle de données une spécification des contraintes de cohérences, qui sont alors automatiquement appliquées.

Cohérence transactionnelle

La figure Un graphe d’objet à rendre persistant montre le graphe d’objet de notre film. Si, dans ce graphe d’objet, une partie seulement est rendu persistante (par exemple le film, en grisé), alors il est clair que la représentation en base de données sera incomplète. Au niveau de la base elle-même, les contraintes d’intégrité référentielle (décrites par les clés étrangères) ne seront pas respectées, et le SGBD rejettera la transaction. Mais avant même cela, Hibernate détectera que la méthode save() est appliquée à un objet transient qui référence d’autres objets transients, avec risque potentiel d’incohérence. Une exception sera donc levée avant même la tentative d’insertion en base.

_images/gravity.png

Figure 2: Un graphe d’objet à rendre persistant

La solution « manuelle » (que vous avez sans doute mise en application dans l’exercice de la session précédente) consiste à appliquer la méthode save() sur chaque objet du graphe, et ce dans un ordre bien défini pour éviter de rendre persistant un objet référençant un objet transient.

Note

Dans les bases objets (peu répandues), cette cohérence transactionnelle est prise en charge par le SGBD qui applique un principe dit de persistance par atteignabilité. Ce principe n’existe pas dans les SGBD relationnelles, la notion la plus proche étant celle d’intégrité référentielle.

L’option cascade sert à spécifier la prise en charge par Hibernate de la cohérence transactionnelle.

L’option cascade

La propagation « en cascade » des instructions de persistance dans un graphe d’objet est spécifiée par une annotation @Cascade.

Important

Dans ce qui suit nous utilisons les options de JPA, et pas celles d’Hibernate. Attention, il semble que JPA et Hibernate soient partiellement incompatibles sur ce point, donc faites bien attention aux packages que vous importez (pour JPA, c’est javax.persistence.*).

De plus, la méthode save() de la session Hibernate semble ne pas reconnaitre les annotations JPA, alors que cela fonctionne bien pour la méthode persist() que nous utilisons donc. Ces problèmes seront probablement réglés dans une prochaine version.

Voici donc simplement comment on indique que le réalisateur doit être rendu persistant quand le film est lui-même rendu persistant (classe Film.java).

@ManyToOne (fetch=FetchType.LAZY, cascade=CascadeType.PERSIST)
@JoinColumn(name = "id_realisateur")
private Artiste realisateur;

Si vous appelez maintenant la méthode session.persist(gravity), la persistance s’appliquera par transitivité au metteur en scène (essayez).

Les opérations de persistance à « cascader » sont les créations, modifications et destructions, et peuvent se placer des deux côtés de l’association. Voici les annotations (JPA) correspondantes, énumérées par CascadeType.

  • PERSIST. C’est l’exemple montré ci-dessus. Quand on appelle la méthode persist() sur l’objet référençant (le film dans notre exemple), l’objet référencé (l’artiste dans notre exemple) devient également persistant par appel à la méthode persist(), qui est donc éventuellement propagée transitivement à son tour.

  • REMOVE. Un appel à remove() sur l’objet référençant est propagé à l’objet référencé.

    Attention à bien considérer cette option. Sur notre schéma, la suppression d’un film ne devrait pas entraîner la suppression du réalisateur (vous êtes bien d’accord?). En revanche la suppression d’un artiste peut entraîner celle des films qu’il/elle a réalisés (à décider à tête reposée), et entraîne certainement celle des rôles qu’il/elle a joués.

  • MERGE. La méthode merge() JPA est équivalente à saveOrUpdate() en Hibernate. Cette option de cascade indique donc une propagation des opérations d’insertion ou de mise à jour sur les objets référencés.

  • REFRESH. La méthode refresh() synchronise l’objet par lecture de son état dans la base. Cette option de cascade indique donc une propagation de l’opération sur les objets référencés.

  • ALL. Couvre l’ensemble des opérations précédentes.

N’utilisez surtout pas CascadeType.ALL aveuglément! La persistance transitive complète s’impose pour les associations de nature compositionnelle (par exemple une commande et ses lignes de commande), dans lesquelles le sort d’un composé est étroitement lié à celui du composant (mais l’inverse n’est pas vrai!). Elle est aussi normale pour les entités qui représentent (conceptuellement) une association plusieurs-plusieurs dans le modèle de l’application. Dans notre schéma, c’est le cas pour les rôles par exemple, dont la préservation n’a pas de sens dès lors que le film ou l’acteur est supprimé. Dans toutes les autres situations, une réflexion au cas par cas s’impose pour éviter de déclencher automatiquement des opérations non souhaitées (et surtout des destructions).

Notez qu’il est possible de combiner plusieurs options en les plaçant entre accolades:

cascade = {CascadeType.PERSIST, CASCADE.MERGE}

Vous voilà équipés pour spécifier la persistance transitive avec JPA/Hibernate.

Exercice: cohérence transactionnelle pour le graphe d’un film

Ajoutez les options cascade à votre modèle pour que la persistance d’un film entraîne automatiquement celle des objets liés. Vérifiez que cela fonctionne en insérant les objets de la figure Un graphe d’objet à rendre persistant.

Résumé: savoir et retenir

La question des mises à jour est souvent plus délicate que celle des recherches, tout simplement parce que son impact sur la base de données est plus grand. Retenez:

  • que toute mise à jour s’effectue dans le contexte d’une transaction, qui s’exécute de manière atomique (tout est validé, ou rien);
  • une transaction peut lever des exceptions qui risquent de laisser la couche ORM dans un état incohérent (synchronisation incomplète entre la base et la session): toute levée d’exception doit entraîner la fermeture de la session courante;
  • les objets passent par divers statuts, distingués techniquement par la présence d’une valeur pour l’identifiant; le passage au statut persistant assure la synchronisation avec la base: essayez de produire un code dans lequel ce statut est clair à chaque instant;
  • la persistance d’un graphe d’objet peut s’obtenir avec des annotations @Cascade, dont l’option plus simple et la plus sûre à manipuler est PERSIST; les autres options sont à considérer avec précaution et en connaissance de cause.

Nous n’en avons pas fini avec les transactions, considérées jusqu’à présent de manière isolée, alors qu’elle s’exécutent souvent en concurrence. C’est le sujet du prochain chapitre.