.. _chap-dynamic: ################################ 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 : * `Diapos pour la session "S1 : Objets transients, persistants, et détachés" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f35951226gpyblcw/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2047 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. .. code-block:: java // 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 :ref:`cyclevie` résume ces interactions et les changements de statut qu'eles entrainent. .. _cyclevie: .. figure:: ../figures/cyclevie.png :width: 90% :align: center 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. .. admonition:: 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. .. code-block:: java 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 : * `Diapos pour la session "S2 : Persistance transitive" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f359512cdh5tip39/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2048 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 :ref:`gravity` 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. .. _gravity: .. figure:: ../figures/gravity.png :width: 70% :align: center 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*). .. code-block:: 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: .. code-block:: java cascade = {CascadeType.PERSIST, CASCADE.MERGE} Vous voilà équipés pour spécifier la persistance transitive avec JPA/Hibernate. .. admonition:: 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 :ref:`gravity`. ************************* 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.