.. _chap-jpamodel: ################# JPA: le *mapping* ################# Nous abordons maintenant une étude plus systématique de JPA/Hibernate en commençant par la définition du modèle de données, ou plus exactement de l'association (*mapping*) entre la base de données relationnelle et le modèle objet java. Nous parlerons plus concisément de *mapping O/R* dans ce qui suit. Rappelons le but de cette spécification: transformer *automatiquement* une base relationnelle en graphe d'objets java. Cette transformation est effectuée par Hibernate sur des données extraites de la base avec des requêtes SQL. .. note:: nous considérons pour l'instant que la base est pré-existante, comme notre base *webscope*. Une autre possibilité, plus radicale, est de définir un modèle objet et de laisser Hibernate générer le schéma relationnel correspondant. Le but de ce chapitre est principalement de finaliser notre modèle java pour la base *webscope*, ce qui couvrira les options les plus courantes du *mapping* O/R. Il existe plusieurs méthodes pour définir un modèle O/R. - par un fichier de configuration XML; - par des annotations directement intégrées au code java. L'option "annotation" présente de nombreux avantages, pour la clarté, les manipulations de fichier, la concision. C'est donc celle qui est présentée ci-dessous. Reportez-vous à la documentation Hibernate si vous voulez inspectez un fichier de configuration XML typique. .. caution:: Attention, quand vous regardez les (innombrables) documentations sur le Web, à la date de publication; les outils évoluent rapidement, et beaucoup de tutoriaux sont obsolètes. Pour utiliser les annotations JPA, il faut inclure le *package* ``javax.persistence.*``. Il nous semble préférable de suivre JPA le plus possible plutôt que la syntaxe spécifique à Hibernate, la tendance de toute façon étant à la convergence de ce dernier vers la norme. ************************* S1: Entités et composants ************************* Supports complémentaires : * `Diapos pour la session "S1 : Entités et composants" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f3594c278sxffxru/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2018 Une entité, déclarée par l'annotation ``@Entity`` définit une classe Java comme étant *persistante* et donc associée à une table dans la base de données. Cette classe doit être implantée selon les normes des beans: propriétés déclarées comme n'étant pas publiques (*private* est sans doute le bon choix), et accesseurs avec *set* et *get*, nommés selon les conventions habituelles. .. note: une entité n'a pas strictement besoin d'hériter de ``Serializable``, sauf si elle doit être transmise à un service distant par le réseau. Nous n'utilisons donc pas cette option ici. Par défaut, une entité est associée à la table portant le même nom que la classe. Il est possible d'indiquer le nom de la table par une annotation ``@Table``. En voici un exemple (parfaitement inutile en l'occurrence): .. code-block:: java @Entity @Table(name="Film") public class Film { ... } De nombreuses options JPA, et plus encore Hibernate, sont disponibles, mais nous allons nous limiter à l'essentiel pour comprendre les mécanismes. Vous pourrez toujours vous reporter à la documentation pour des besoins ponctuels. La norme JPA indique qu'il est nécessaire de créer un constructeur vide pour chaque entité. Le *framework* peut en effet avoir à instancier une entité avec un appel à ``Constructor.newInstance()`` et le constructeur correspondant doit exister. La norme JPA indique que ce constructeur doit être public ou protégé: .. code-block:: java public Film() {} La méthode ci-dessus a l'inconvénient de créer un objet dans un état invalide (aucune propriété n'a de valeur). Cela ne pose pas de problème quand c'est Hibernate qui utilise ce constructeur puisqu'il va affecter les propriétés en fonction des valeurs dans la base, mais du point de vue de l'application c'est une source potentielle d'ennuis. Avec Hibernate, le constructeur vide peut être déclaré ``private`` (mais ce n'est pas la recommandation JPA). Finalement, il est recommandé, au moins pour certaines classes, d'implanter des méthodes *equals()* et *hashCode()* pour qu'Hibernate puisse déterminer si deux objets correspondent à la même ligne de la base. C'est un sujet un peu trop avancé pour l'instant, et nous le laissons donc de côté. .. important:: Nous nous soucions essentiellement pour l'instant d'opération de *lecture*, qui suffisent à illustrer et valider la modélisation du *mapping*. Identifiant d'une entité ======================== Toute entité doit avoir une propriété déclarée comme étant l'identifiant de la ligne dans la table correspondante. Il est beaucoup plus facile de gérer une clé constituée d'une seule valeur qu'une clé composée de plusieurs. Les bases de données conçues selon des principes de *clé artificielle* ou *surrogate key* (produite à partir d'une séquence) sont de loin la meilleure solution. On peut être confronté à une base qui n'a pas été conçue sur ce principe, auquel cas JPA/Hibernate fournit des méthodes permettant de faire face à la situation, mais c'est plus compliqué. Nous présentons une solution à la fin de ce chapitre. L'identifiant est indiqué avec l'annotation ``@Id``. Pour produire automatiquement les valeurs d'identifiant, on ajoute une annotation ``@GeneratedValue`` avec un paramètre ``Strategy``. Voici deux possibilités pour ce paramètre: * ``Strategy = GenerationType.AUTO``. Hibernate produit lui-même la valeur des identifiants grâce à une table ``hibernate_sequence``. * ``Strategy = GenerationType.IDENTITY``. Hibernate s'appuie alors sur le mécanisme propre au SGBD pour la production de l'identifiant. Dans le cas de MySQL, c'est l'option ``AUTO-INCREMENT``, dans le cas de postgres ou Oracle, c'est une séquence. C'est à vous de vous assurer que pour chaque table, ce mécanisme est en place. Voici les commandes pour ``Film`` ou ``Artiste`` en utilisant le mécanisme automatique d'Hibernate, *en supposant que les clés primaires ne sont pas auto-incrémentées*. .. code-block:: java @Id @GeneratedValue(Strategy = GenerationType.AUTO) private Integer id; private void setId(Integer i) { id = i; } public Integer getId() { return id; } .. note:: en stratégie ``GenerationType.AUTO``, Hibernate doit se charger de créer une table ``hibernate_sequence``. Si la clé est auto-incrémentée dans MySQL, l'annotation est la suivante. .. code-block:: java @Id @GeneratedValue(Strategy = GenerationType.IDENTITY) private Integer id; Faut-il fournir des accesseurs ``setId()`` et ``getId()``? Conceptuellement, l'id est utilisé pour lier un objet à la base de données et ne joue aucun rôle dans l'application. Il n'a donc pas de raison d'apparaître publiquement. d'où l'absence de méthode pour le récupérer dans la plupart des objets métiers. - fournir une méthode *publique* ``setId()`` pour un identifiant auto-généré ne semble pas une bonne idée, car dans ce cas l'application pourrait affecter un identifiant en conflit avec la méthode de génération; il est donc préférable de créer une méthode privée; - fournir une méthode ``getId()`` n'est pas nécessaire, sauf si l'application souhaite inspecter la valeur de l'identifiant en base de données, ce qui est de fait souvent utile. En résumé, la méthode ``setId()``devrait être ``private``. Notez qu'Hibernate utilise l'API ``Reflection`` de java pour acéder aux propriétés des objets, et n'a donc pas besoin de méthodes publiques. .. note:: Avec Hibernate, l'existence de ``setId()`` ne semble même pas nécessaire. Il semble que JPA requiert des accesseurs pour chaque propriété mappée, donc autant respecter cette règle pour un maximum de compatibilité. Les colonnes ============ Par défaut, toutes les propriétés non-statiques d'une classe-entité sont considérées comme devant être stockées dans la base. Pour indiquer des options (et aussi pour des raisons de clarté à la lecture du code) on utilise le plus souvent l'annotation ``@Column``, comme par exemple: .. code-block:: java @Column private String nom; public void setNom(String n) {nom= n;} public String getNom() {return nom;} Cette annotation est utile pour indiquer le nom de la colonne dans la table, quand cette dernière est différente du nom de la propriété en java. Dans notre cas nous utilisons des règles de nommage différentes en java et dans la base de données pour les noms composés de plusieurs mots. ``@Column`` permet alors d'établir la correspondance: .. code-block:: java @Column(name="annee_naissance") private Integer anneeNaissance; public void setAnneeNaissance(Integer a) {anneeNaissance = a;} public Integer getAnneeNaissance() {return anneeNaissance;} Voici les principaux attributs pour ``@Column``. - ``name`` indique le nom de la colonne dans la table; - ``length`` indique la taille maximale de la valeur de la propriété; - ``nullable`` (avec les valeurs ``false`` ou ``true``) indique si la colonne accepte ou non des valeurs à ``NULL`` (au sens "base de données" du terme: une valeur à ``NULL`` est une absence de valeur); - ``unique`` indique que la valeur de la colonne est unique. .. note:: Un objet métier peut très bien avoir des propriétés que l'on ne souhaite pas rendre persistantes dans la base. Il faut alors impérativement les marquer avec l'annotation ``@Transient``. Les composants ============== Une *entité* existe par elle-même indépendamment de toute autre entité, et peut être rendue persistante par insertion dans la base de données, avec un identifiant propre. Un *composant*, au contraire, est un objet sans identifiant, qui ne peut être persistant que par rattachement (direct ou transitif) à une entité. La notion de composant résulte du constat qu'une ligne dans une base de données peut *parfois* être décomposée en plusieurs sous-ensemble dotés chacun d'une logique autonome. Cette décomposition mène à une granularité fine de la représentation objet, dans laquelle on associe *plusieurs* objets à *une* ligne de la table. On peut sans doute modéliser une application sans recourir aux composants, au moins dans la définition stricte ci-dessus. Ils sont cependant également utilisés dans Hibernate pour gérer d'autres situations, et notamment les clés composées de plusieurs attributs, comme nous le verrons plus loin. Regardons donc un exemple concret, celui (beaucoup utilisé) de la représentation des *adresses*. Prenons donc nos internautes, et ajoutons à la table quelques champs (au minimum) pour représenter leur adresse. .. code-block:: sql ALTER TABLE Internaute ADD adresse TEXT, ADD code_postal VARCHAR(10), ADD ville VARCHAR(100); Utilisez phpMyAdmin pour insérer quelques adresses aux internautes existant dans la base, et passons maintenant à la modélisation java. On va considérer ici que l'adresse est un *composant* de la représentation d'un internaute qui dispose d'une unité propre et distinguable du reste de la table. Cela peut se justifier, par exemple, par la nécessité de contrôler qu'un code postal est correct, une logique applicative qui n'est pas vraiment pertinente pour l'entité *Internaute*. Un autre argument est qu'on peut réutiliser la définition du composant pour d'autres entités, comme par exemple ``Société``. Toujours est-il que nous décidons de représenter une adresse comme un composant, désigné par le mot-clé ``Embeddable`` en JPA. .. code-block:: java package modeles.webscope; import javax.persistence.*; @Embeddable public class Adresse { String adresse; public void setAdresse(String v) {adresse = v;} public String getAdresse() {return adresse;} @Column(name="code_postal") String codePostal; public void setCodePostal(String v) {codePostal = v;} public String getCodePostal() {return codePostal;} String ville; public void setVille(String v) {ville = v;} public String getVille() {return ville;} } La principale différence visible avec une entité est qu'un composant n'a pas d'identifiant (marqué ``@Id``) et ne peut donc pas être sauvegardé dans la base indépendamment. Il faut au préalable le rattacher à une entité, ici ``Internaute``. .. code-block:: java package modeles.webscope; import javax.persistence.*; @Entity public class Internaute { @Id private String email; public void setEmail(String e) {email = e;} @Column private String nom; public void setNom(String n) {nom = n;} public String getNom() {return nom;} @Column private String prenom; public void setPrenom(String p) {prenom = p;} public String getPrenom() { return prenom;} @Embedded private Adresse adresse; public void setAdresse(Adresse a) {adresse = a;} public Adresse getAdresse() {return adresse;} } Au lieu de l'annotation ``Column``, on utilise ``@Embedded`` et le tour est joué. La propriété ``adresse`` devient, comme ``nom`` et ``prénom``, une propriété *persistante* de l'entité. .. _ex-jpamodel-composants: .. admonition:: Exercice: affichez la liste des internautes. Implantez une action qui affiche la liste des internautes avec leur adresse. Vous remarquerez que la classe ``Adresse`` contient des annotations qui le *mappent* vers la table, notamment avec les noms de colonne. Mais que se passe-t-il alors si on place deux composants du même type dans une entité? Par exemple, si on veut avoir une adresse personnelle et une adresse professionnelle pour un internaute? On *mapperait* dans ce cas deux propriétés distinctes vers la *même* colonne. Pour illustrer le problème (et la solution), commençons par modifier la table. .. code-block:: sql ALTER TABLE Internaute ADD adresse_pro TEXT, ADD code_postal_pro VARCHAR(10), ADD ville_pro VARCHAR(100); On indique alors, *dans le composé* (l'entité), le *mapping* entre la table de l'entité et le nouveau composant, autrement dit les colonnes de la table qui doivent stocker les propriétés du composant. C'est une *surcharge* (*override* en anglais), d'où la syntaxe suivante en JPA, à ajouter à la classe ``Internaute``. .. code-block:: java @Embedded @AttributeOverrides( { @AttributeOverride(name="adresse", column = @Column(name="adresse_pro") ), @AttributeOverride(name="codePostal", column = @Column(name="code_postal_pro") ), @AttributeOverride(name="ville", column = @Column(name="ville_pro") ) } ) private Adresse adressePro; public void setAdressePro(Adresse a) {adressePro = a;} public Adresse getAdressePro() {return adressePro;} L'attribut ``codePostal`` du composant ``adressePro`` sera donc par exemple stocké dans la colonne ``code_postal_pro``. .. _ex-jpamodel-composants-bis: .. admonition:: Exercice: affichez les adresses personnelle et professionelle. Etendez l'action précédente pour affichez les deux adresses. Au préalable, utilisez phpMyAdmin pour saisir quelques valeurs d'adresse professionnelle. .. _ex-jpamodel-entites: .. admonition:: Exercice: mappez toutes les entités de la base *webscope* Définissez une classe pour chaque *entité* de la base *webscope*, avec un *mapping* utlisant les annotations vues jusqu'à présent. Attention: nous parlons bien des *entités* (cf. le modèle UML) et pas des associations qui parfois, sont aussi représentées par des tables. ******************************* S2: associations un-à-plusieurs ******************************* Supports complémentaires : * `Diapos pour la session "S2 : associations un-à-plusieurs" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f3595092c8o8in5x/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2019 Venons-en maintenant aux associations. Nous laissons de côté les associations "un à un" qui sont peu fréquentes et nous en venons directement aux associations "un à plusieurs". Notre exemple prototypique dans notre base de données est la relation entre un film et son (unique) réalisateur. Un premier constat, très important: en java nous pouvons représenter l'association de trois manières, illustrées sur la figure :ref:`bidirect`. - dans un film, on place un lien vers le réalisateur (unidirectionnel, à gauche); - dans un artiste, on place des liens vers les films qu'il a réalisés (unidirectionnel, centre); - on représente les liens des deux côtés (bidirectionnel, droite). Rappelons que dans une base relationnelle, le problème ne se pose pas: une association représentée par une clé étrangère est par nature bidirectionnelle. Cette différence est due aux deux paradigmes opposés sur lesquels s'appuient le modèle relationnel d'une part (qui considère des *ensembles* sans liens explicites entre eux, une association étant reconstitué par un calcul de jointure) et le modèle objet de java (qui établit une navigation dans les objets instantiés grâce aux références d'objets). .. _bidirect: .. figure:: ../figures/bidirect.png :width: 60% :align: center Trois possibilités pour une association un à plusieurs Ceci étant posé, voyons comment nous pouvons représenter les trois situations, pour un même état de la base de données. L'annotation ``@ManyToOne`` =========================== Nous avons déjà étudié ce cas de figure. Dans la classe ``Film``, nous indiquons qu'un objet lié nommé ``réalisateur``, de la classe ``Artiste``, doit être cherché dans la base par Hibernate. .. code-block:: java @ManyToOne @JoinColumn (name="id_realisateur") private Artiste realisateur; public void setRealisateur(Artiste a) {realisateur = a;} public Artiste getRealisateur() {return realisateur;} L'annotation ``@JoinColumn`` indique quelle est la *clé étrangère* dans ``Film`` qui permet de rechercher l'artiste concerné. En d'autres termes, quand on appelle ``getRealisateur()``, Hibernate doit engendrer et exécuter la requête suivante; .. code-block:: sql select * from Artiste where id = ?film.id_realisateur où ``?film`` désigne l'objet-film courant. .. important:: Vous pouvez noter qu'avec le comportement simpliste décrit ci-dessus, on effectue une requête SQL pour rechercher *un* objet, ce qui constitue une utilisation totalement sous-optimale de SQL et risque d'engendrer de très gros problèmes de performance pour des bases importantes. Gardez cette remarque sous le coude, nous y reviendrons plus tard. Hibernate sait bien entendu représenter l'association (clé primaire / clé étrangère) dans la base, en transposant la référence objet d'un artiste par un film. Voici concrètement comment cela se passe. Créons l'action ``insertFilm`` ci-dessous. .. code-block:: java public void insertFilm() { session.beginTransaction(); Film gravity = new Film(); gravity.setTitre("Gravity"); gravity.setAnnee(2013); Genre genre = new Genre(); genre.setCode("Science-fiction"); gravity.setGenre(genre); Artiste cuaron = new Artiste(); cuaron.setPrenom("Alfonso"); cuaron.setNom("Cuaron"); // Le réalisateur de Gravity est Alfonso Cuaron gravity.setRealisateur(cuaron); // Sauvegardons dans la base session.save(gravity); session.save(cuaron); session.getTransaction().commit(); } On a donc créé le film *Gravity*, de genre *Science-Fiction* et mis en scène par Alfonso Cuaron. L'association est créée, *au niveau du graphe d'objets java* par l'instruction:: gravity.setRealisateur(cuaron); On a ensuite sauvegardé les deux nouvelles entités dans la base. Exécutons cette action, et regardons ce qui s'affiche dans la console: .. code-block:: sql Hibernate: select genre_.code from Genre genre_ where genre_.code=? Hibernate: insert into Film (annee, genre, code_pays, d_realisateur, resume, titre) values (?, ?, ?, ?, ?, ?) Hibernate: insert into Artiste (annee_naissance, nom, prenom) values (?, ?, ?) Hibernate: update Film set annee=?, genre=?, code_pays=?, id_realisateur=?, resume=?, titre=? where id=? Ce que l'on voit: Hibernate a engendré et exécuté des requêtes SQL. La première sélectionne le genre *Science-Fiction*: comme nous avons indiqué la valeur de la clé primaire dans l'instance Java, Hibernate vérifie automatiquement si cet objet ``Genre`` existe dans la base et va le lire si c'est le cas opur le lier par référence à l'objet ``Film`` en cours de création. Hibernate cherche toujours à synchroniser le graphe des objets et la base de données quand c'est nécessaire. Le premier ``insert`` est déclenché par l'instruction ``session.save(gravity)``. Il insère le film. À ce stade l'identifiant du metteur en scène dans la base est à ``NULL`` puisque Cuaron n'a pas encore été sauvegardé. Nous pouvons donc, à un moment donné, associer un objet persistant (le film ici) à un objet transient (l'artiste). Le second ``insert`` correspond à ``session.save(cuaron)``. Il insère l'artiste, qui obtient alors un identifiant dans la base de données. Et du coup Hibernate effectue un ``update`` sur le film pour affecter cet identifiant à la colonne ``id_realisateur``. .. note:: On remarque que tous les champs de ``Film`` sont modifiés par l'``update`` alors que seul l'identifiant du metteur en scène a changé. Hibernate ne gère pas les modifications au niveau des attributs mais se contente - c'est déjà beaucoup - de détecter toute modification de l'objet java pour déclencher une synchronisation avec la base. Nous reviendrons sur tout cela quand nous parlerons des transactions. En résumé, tout va bien. Pour un effort minime nous obtenons une gestion complète de l'association entre un film et son metteur en scène. Hibernate synchronise les actions effectuées sur le graphe des objets persistants en java avec la base de données, que ce soit en lecture ou en écriture. C'est aussi le bon endroit pour noter concrètement la différence, en ce qui concerne les associations, entre la représentation relationnelle et le modèle de données en java. Il n'est pour l'instant pas possible de récupérer la liste des films réalisés par Alfonson Cuaron car nous n'avons pas représenté dans le modèle objet (Java) ce côté de l'association. En revanche, une simple jointure en SQL nous donnerait cette information. L'annotation ``@OneToMany`` =========================== De l'autre côté de l'association, on utilise l'annotation ``@OneToMany``: à un objet correspondent plusieurs objets associés. Dans notre exemple, à un artiste correspondent de 0 à plusieurs films réalisés. .. important:: Mettez en commentaires, pour l'instant, la clause ``@ManyToOne`` définissant le réalisateur dans la classe ``Film``, puisque nous étudions les associations unidirectionnelles: .. code-block:: java /* @ManyToOne @JoinColumn (name="id_realisateur") private Artiste realisateur; public void setRealisateur(Artiste a) {realisateur = a;} public Artiste getRealisateur() {return realisateur;} */ Nous allons voir dans la prochaine session ce qui se passe quand on combine les deux. Spécifions l'association ``@OneToMany`` dans la classe ``Artiste``. .. code-block:: java @OneToMany @JoinColumn(name="id_realisateur") private Set filmsRealises = new HashSet(); public void addFilmsRealise(Film f) {filmsRealises.add(f) ;} public Set getFilmsRealises() {return filmsRealises;} Il n'y a pratiquement pas de différence avec la représentation ``@ManyToOne``. On indique, comme précédemment, que la clé étrangère est ``id_realisateur`` et Hibernate comprend qu'il s'agit d'un attribut de ``Film``, ce qui lui permet d'engendrer la même requête SQL que précédemment. L'association est représentée par une collection, ici un ``Set`` (notez qu'on l'initialise avec un ensemble vide). On fournit deux méthodes, ``add`` (au lieu de ``set`` pour le côté ``@ManyToOne``) et ``get``. Tentons à nouveau l'insertion d'un nouveau film et de son metteur en scène. Il suffit d'exécuter la même méthode ``insertFilm`` que précédemment, en remplaçant la ligne:: gravity.setRealisateur(cuaron); par la ligne:: cuaron.addFilmsRealise(gravity); Supprimez éventuellement les données insérées précédemment, dans phpMyAdmin, avec: .. code-block:: sql DELETE FROM Film where titre='Gravity'; DELETE FROM Artiste WHERE nom='Cuaron' et exécutez cette action. Hibernate devrait afficher les requêtes suivantes dans la console: .. code-block:: sql Hibernate: select genre_.code from Genre genre_ where genre_.code=? Hibernate: insert into Film (annee, genre, code_pays, id_realisateur, resume, titre) values (?, ?, ?, ?, ?, ?) Hibernate: insert into Artiste (annee_naissance, nom, prenom) values (?, ?, ?) Hibernate: update Film set id_realisateur=? where id=? Hibernate a effectué les insertions et les mises à jour identiques à celles déjà vues pour l'association ``@ManyToOne``. Là aussi, c'est normal puisque nous avons changé l'association en java, mais au niveau de la base elle reste représentée de la même manière. Cette fois il n'est plus possible de connaître le metteur en scène d'un film puisque nous avons supprimé un des côtés de l'association. Il va vraiment falloir représenter une association bidirectionnelle. C'est ce que nous étudions dans la prochaine session. Faites au préalable l'exercice ci-dessous. .. _ex-jpamodel-artistes: .. admonition:: Exercice: afficher la liste des artistes avec les films qu'ils ont réalisés Ecrire une action qui affiche tous les artistes et la liste des films qu'ils ont réalisés, quand c'est le cas. Aide: on devrait donc trouver deux boucles imbriquées dans la JSTL, une sur les artistes, l'autre sur les films. ********************************** S3: Associations bidirectionnelles ********************************** Supports complémentaires : * `Diapos pour la session "S3 : Associations bidirectionnelles" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f35950b5fhlm5fi0/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2020 Maintenant, que se passe-t-il si on veut représenter l'association de manière bi-directionnelle, ce qui sera souhaité dans une bonne partie des cas. C'est tout à fait pertinent par exemple pour notre exemple, puisqu'on peut très bien vouloir naviguer dans l'association ``réalise`` à partir d'un metteur en scène ou d'un film. On peut tout à fait représenter l'association des deux côtés, en plaçant donc simultanément dans la classe ``Film`` la spécification: .. code-block:: java @ManyToOne @JoinColumn (name="id_realisateur") private Artiste realisateur; public void setRealisateur(Artiste a) {realisateur = a;} public Artiste getRealisateur() {return realisateur;} et dans la classe ``Artiste`` la spécification: .. code-block:: java @OneToMany @JoinColumn(name="id_realisateur") private Set filmsRealises = new HashSet(); public void addFilmsRealise(Film f) {filmsRealises.add(f) ;} public Set getFilmsRealises() {return filmsRealises;} Cette fois l'association est représentée des deux côtés. Le problème =========== Cela soulève cependant un problème dû au fait que là où il y a deux emplacements distincts en java, il n'y en a qu'un en relationnel. Effectuons une dernière modification de ``insertFilm()`` en affectant les *deux* côtés de l'assocation. .. code-block:: java // Le réalisateur de Gravity est Alfonso Cuaron gravity.setRealisateur(cuaron); // Films réalisés par A. Curaon? for (Film f : cuaron.getFilmsRealises()) { System.out.println("Curaron a réalisé " + f.getTitre()); } // Alfonso Cuaron a réalisé Gravity cuaron.addFilmsRealise(gravity); Au passage, on a aussi affiché la liste des films réalisés par Alfonso Caron. Première remarque: le code est alourdi par la nécessité d'appeler ``setRealisateur()`` *et* ``addFilmRealises()`` pour attacher les deux bouts de l'association aux objets correspondants. Exécutez à nouveau ``insertFilm()`` (après avoir supprimé à nouveau de la base l'artiste et le film) et regardons ce qui se passe. Hibernate affiche: .. code-block:: sql Hibernate: select genre_.code from Genre genre_ where genre_.code=? Hibernate: insert into Film (annee, genre, code_pays, id_realisateur, resume, titre) values (?, ?, ?, ?, ?, ?) Hibernate: insert into Artiste (annee_naissance, nom, prenom) values (?, ?, ?) Hibernate: update Film set annee=?, genre=?, code_pays=?, id_realisateur=?, resume=?, titre=? where id=? Hibernate: update Film set id_realisateur=? where id=? Avez-vous d'abord noté ce que *l'on ne voit pas*? Normalement notre code devrait afficher la liste des films réalisés par Alfonso Cuaron. Rien ne s'affiche, car nous avons demandé cet affichage *après* avoir dit que Cuaron est le réalisateur de *Gravity*, mais *avant* d'avoir dit que *Gravity* fait partie des films réalisés par Cuaron. En d'autres termes, java *ne sait pas*, quand on renseigne un côté de l'association, *déduire* l'autre côté. Cela semble logique quand on y réfléchit, mais ça va mieux en le disant et en le constatant. Il faut donc bien appeler ``setRealisateur()`` *et* ``addFilmRealises()`` pour que notre graphe d'objets java soit cohérent. Si on ne le fait, l'incohérence du graphe est potentiellement une source d'erreur pour l'application. Seconde remarque: Hibernate effectue deux ``update``, alors qu'un seul suffirait. Là encore, c'est parce que les deux spécifications de chaque bout de l'association sont indépendantes l'une de l'autre. Ouf. Prenez le temps de bien réfléchir, car nous sommes en train d'analyser une des principales difficultés conceptuelles du *mapping* objet-relationnel. (réflexion intense de votre part) Le remède ========= Bien, pour résumer nous avons deux objets java qui sont déclarés comme persistants (le fim, l'artiste). Hibernate surveille ces objets et déclenche une mise à jour dans la base dès que leur état est modifié. Comme la réalisation d'une association implique la modification des *deux* objets, alors que la base relationnelle représente l'association en un seul endroit (la clé étrangère), on obtient des mises à jour redondantes. Est-ce *vraiment* grave? A priori on pourrait vivre avec, le principal inconvénient prévisible étant un surplus de requêtes transmises à la base, ce qui peut éventuellement pénaliser les performances. Ce n'est pas non plus très satisfaisant, et Hibernate propose une solution: déclarer qu'un côté de l'association est *responsable* de la mise à jour. Cela se fait avec l'attribut ``mappedBy``, comme suit. .. code-block:: java @OneToMany(mappedBy="realisateur") private Set filmsRealises = new HashSet(); On indique donc que le *mapping* de l'association est pris en charge par la classe ``Film``, *et on supprime l'annotation* ``@JoinColumn`` *puisqu'elle devient inutile*. Ce que dit ``mappedBy`` en l'occurrence, c'est qu'un objet associé de la classe ``Film`` maintient un lien avec une instance de la classe courante (un ``Artiste``) grâce à une propriété nommée ``realisateur``. Hibernate, en allant inspecter la classe ``Film``, trouvera que ``realisateur`` est *mappé* avec la base de données par le biais de la clé étrangère ``id_realisateur``. Dans ``Film``, on trouve en effet: .. code-block:: java @ManyToOne @JoinColumn (name="id_realisateur") private Artiste realisateur; Toutes les informations nécessaires sont donc disponibles, et représentées une seule fois. Cela semble compliqué? Oui, ça l'est quelque peu, sans doute, mais encore une fois il s'agit de la partie la plus contournée du *mapping* ORM. Prenez l'exemple que nous sommes en train de développer comme référence, et tout ira bien. En conséquence, une modification de l'état de l'association au niveau d'un artiste *ne déclenchera pas* d'``update`` dans la base de données. En d'autres termes, l'instruction .. code-block:: java // Alfonso Cuaron a réalisé Gravity cuaron.addFilmsRealise(gravity); *ne sera pas* synchronisée dans la base, et rendue persistante. On a sauvé un ``update``, mais introduit une source d'incohérence. Une petite modification de la méthode ``addFilmsRealises()`` suffit à prévenir le problème. .. code-block:: java public void addFilmsRealise(Film f) { f.setRealisateur(this); filmsRealises.add(f) ; } L'astuce consiste à appeler ``setRealisateur()`` pour garantir la mise à jour dans la base dans tous les cas. En résumé ========= Il est sans doute utile de résumer ce qui précède. Voici donc la méthode recommandée pour une association *un à plusieurs*. Nous prenons comme guide l'association entre les films et leurs réalisateurs. ``Film`` est du côté ``plusieurs`` (un réalisateur met en scène *plusieurs* films), ``Artiste`` du côté ``un`` (un film a *un* metteur en scène). Dans la base relationnelle, c'est du côté *plusieurs* que l'on trouve la clé étrangère. **Du côté "plusieurs"** On donne la spécification du *mapping* avec la base de données. Les annotations sont ``@ManyToOne`` et ``@JoinColumn``. Par exemple. .. code-block:: java @ManyToOne @JoinColumn (name="id_realisateur") private Artiste realisateur; Dans le jargon ORM, ce côté est "responsable" de la gestion du *mapping*. **Du côté "un"** On indique avec quelle *classe responsable* on est associé, et quelle *propriété* dans cette classe représente l'association. Aucune référence directe à la base de données n'est faite. L'annotation est ``@OneToMany`` avec l'option ``mappedBy`` qui indique que la responsabilité de l'association est déléguée à la classe référencée, dans laquelle l'association elle-même est représentée par la valeur de l'attribut ``mappedBy``. Par exemple: .. code-block:: java @OneToMany(mappedBy="realisateur") private Set filmsRealises = new HashSet(); Hibernate inspectera la propriété ``realisateur`` dans la classe responsable pour déterminer les informations de jointure si nécessaire. **Les accesseurs** Du côté ``plusieurs``, les accesseurs sont standards. Pour la classe ``Film``. .. code-block:: java public void setRealisateur(Artiste a) {realisateur = a;} public Artiste getRealisateur() {return realisateur;} et pour la classe ``Artiste``, on prend soin d'appeler l'accesseur de la classe responsable pour garantir la synchronisation avec la base et la cohérence du graphe. .. code-block:: java public void addFilmsRealise(Film f) { f.setRealisateur(this); filmsRealises.add(f) ; } public Set getFilmsRealises() {return filmsRealises;} Et voilà! Dans ces conditions, voici le code pour créer une association. .. code-block:: java public void insertFilm() { session.beginTransaction(); Film gravity = new Film(); gravity.setTitre("Gravity"); gravity.setAnnee(2013); Genre genre = new Genre(); genre.setCode("Science-fiction"); gravity.setGenre(genre); Artiste cuaron = new Artiste(); cuaron.setPrenom("Alfonso"); cuaron.setNom("Cuaron"); // Alfonso Cuaron a réalisé Gravity cuaron.addFilmsRealise(gravity); // Sauvegardons dans la base session.save(gravity); session.save(cuaron); session.getTransaction().commit(); } L'association est réalisée des deux côtés en java par un seul appel à la méthode ``addFilmsRealises()``. Notez également qu'il reste nécessaire de sauvegarder individuellement le film et l'artiste. Nous pouvons gérer cela avec des annotations ``Cascade``, mais vous avez probablement assez de concepts à avaler pour l'instant. Lisez et relisez ce qui précède. Pas de panique, il suffit de reproduire la même construction à chaque fois. Bien entendu c'est plus facile si l'on a *compris* le pourquoi et le comment. .. admonition:: Exercice: vérifier qu'Hibernate ne génére plus de requête redondante. Modifiez ``insertFilm()`` comme indiqué, et consultez les requêtes Hibernate pour vérifier qu'un seul ``update`` est effectué. Que se passe-t-il si l'application appelle directement ``setRealisateur()`` et pas ``addFilmsRealise()``? Que faudrait-il faire? ************************************** S4: Associations plusieurs-à-plusieurs ************************************** Supports complémentaires : * `Diapos pour la session "S4 : Associations plusieurs-à-plusieurs" `_ * Vidéo associée : https://mediaserver.cnam.fr/permalink/v125f35950bdesbxcgui/ .. * Vidéo associée : https://avc.cnam.fr/univ-r_av/avc/courseaccess?id=2021 Nous en venons maintenant aux associations de type "plusieurs-à-plusieurs", dont deux représentants apparaissent dans le modèle de notre base *webscope*: - un film a *plusieurs* acteurs; un acteur joue dans *plusieurs* films; - un internaute note *plusieurs* films; un film est noté par *plusieurs* internautes. Quand on représente une association plusieurs-plusieurs en relationnel, l'association devient une table dont la clé est la concaténation des clés des deux entités de l'association. C'est bien ce qui a été fait pour cette base: une table ``Role`` représente la première association avec une clé composée ``(id_film, id_acteur)`` et une table ``Notation`` représente la seconde avec une clé ``(id_film, email)``. Et il faut noter de plus que chaque partie de la clé est elle-même une clé étrangère. On pourrait généraliser la règle à des associations ternaires, et plus. Les clés deviendraient très compliquées et le tout deviendrait vraiment difficile à interpréter. La recommandation dans ce cas est de *promouvoir* l'association en entité, en lui donnant un identifiant qui lui est propre. .. note:: Même pour les associations binaires, il peut être justicieux de transformer toutes les associations plusieurs-plusieurs en entité, avec deux associations un-plusieurs vers les entités originales. On aurait donc une entité ``Role`` et une entité ``Notation`` (chacune avec un identifiant). Cela simplifierait le *mapping* JPA qui, comme on va le voir, est un peu compliqué. On trouve de nombreuses bases (dont la nôtre) incluant la représentation d'associations plusieurs-plusieurs avec clé composée. Il faut donc savoir les gérer. Deux cas se présentent en fait: - l'association n'est porteuse d'aucun attribut, et c'est simple; - l'association porte des attributs, et ça se complique un peu. Nos associations sont porteuses d'attributs, mais nous allons quand même traiter les deux cas par souci de complétude. Premier cas: association sans attribut ====================================== Dans notre schéma, le nom du rôle d'un acteur dans un film est représenté dans l'association (un peu de réflexion suffit à se convaincre qu'il ne peut pas être ailleurs). Supposons pour l'instant que nous décidions de sacrifier cette information, ce qui nous ramène au cas d'une association sans attribut. La modélisation JPA/Hibernate est alors presque semblable à celle des associations un-plusieurs. Voici ce que l'on représente dans la classe ``Film``. .. code-block:: java @ManyToMany() @JoinTable(name = "Role", joinColumns = @JoinColumn(name = "id_film"), inverseJoinColumns = @JoinColumn(name = "id_acteur")) Set acteurs = new HashSet(); public Set getActeurs() { return acteurs; } C'est ce côté qui est "responsable" du *mapping* avec la base de données. On indique donc une association ``@ManyToMany``, avec une seconde annotation ``JoinTable`` qui décrit tout simplement la table représentant l'association, ``joinColumn`` étant la clé étrangère pour la classe courante (``Film``) et ``inverseJoinColumn`` la clé étrangère vers la table représentant l'autre entité, ``Artiste``. Essayez de produire la requête SQL qui reconstitue l'association et vous verrez que toutes les informations nécessaires sont présentes. De l'autre côté, c'est plus simple encore puisqu'on ne répète pas la description du *mapping*. On indique avec ``mappedBy`` qu'elle peut être trouvée dans la classe ``Film``. Ce qui donne, pour la filmographie d'un artiste: .. code-block:: java @ManyToMany(mappedBy = "acteurs") Set filmo; public Set getFilmo() { return filmo; } C'est tout, il est maintenant possible de naviguer d'un film vers ses acteurs et réciproquement. L'interprétation de ``mappedBy`` est la même que dans le cas des associations un-plusieurs. .. _ex-jpamodel-acteurs: .. admonition:: Exercice: afficher tous les acteurs d'un film. Reprenez l'action qui affiche les films, et ajoutez la liste des acteurs. De même, pour la liste des artistes, affichez les films dans lesquels ils ont joué. Second cas: association avec attribut ===================================== Maintenant, nous reprenons notre base, et nous aimerions bien accéder au nom du rôle, ce qui nécessite un accès à la table ``Role`` representant l'association. Nous allons représenter dans notre modèle JPA cette association comme une ``@Entity``, avec des associations un à plusieurs vers, respectivement, ``Film`` et ``Artiste``, et le tour est joué. Oui, mais... nous voici confrontés à un problème que nous n'avons pas encore rencontré: la clé de ``Role`` est composée de deux attributs. Il va donc falloir tout d'abord apprendre à gérer ce cas. Nous avons fait déjà un premier pas dans ce sens en étudiant les *composants* (vous vous souvenez, les adresses?). Une clé en Hibernate se représente par un objet. Quand cet objet est une valeur d'une classe de base (``Integer``, ``String``), tout va bien. Sinon, il faut définir cette classe avec les attributs constituant la clé. En introduisant un nouveau niveau de granularité dans la représentation d'une entité, on obtient bien ce que nous avons appelé précédemment un composant. La figure :ref:`composite-id` montre ce que nous allons créer. La classe ``Role`` est une entité composée d'une clé et d'un attribut, le nom du rôle. La clé est une instance d'une classe ``RoleId`` constituée de l'identifiant du film et de l'identifiant de l'acteur. .. _composite-id: .. figure:: ../figures/composite-id.png :width: 60% :align: center Gestion d'un identifiant composé Voici la méthode illustrée par la pratique. Tout d'abord nous créons cette classe représentant la clé. .. code-block:: java package modeles.webscope; import javax.persistence.*; @Embeddable public class RoleId implements java.io.Serializable { /** * */ private static final long serialVersionUID = 1L; @ManyToOne @JoinColumn(name = "id_acteur") private Artiste acteur; public Artiste getActeur() { return acteur; } public void setActeur(Artiste a) { this.acteur = a; } @ManyToOne @JoinColumn(name = "id_film") private Film film; public Film getFilm() { return film; } public void setFilm(Film f) { this.film = f; } } Notez que ce n'est pas une entité, mais un composant annoté avec ``@Embeddable`` (reportez-vous à la section sur les composants si vous avez un doute). Pour le reste on indique les associations ``@ManyToOne`` de manière standard. .. important:: une classe dont les instances vont servir d'identifiants doit hériter de ``serializable``, la raison - technique - étant que des structures de *cache* dans Hibernate doivent être sérialisées dans la session, et que ces structures indexent les objets par la clé. Voici maintenant l'entité ``Role``. .. code-block:: java package modeles.webscope; import javax.persistence.*; @Entity public class Role { @Id RoleId pk; public RoleId getPk() { return pk; } public void setPk(RoleId pk) { this.pk = pk; } @Column(name="nom_role") private String nom; public void setNom(String n) {nom= n;} public String getNom() {return nom;} } On indique donc que l'identifiant est une instance de ``RoleId``. On la nomme ``pk`` pour *primary key*, mais le nom n'a pas d'importance. La classe comprend de plus les attributs de l'association. Et cela suffit. On peut donc maintenant accéder à une instance ``r`` de rôle, afficher son nom avec ``r.nom`` et même accéder au film et à l'acteur avec, respectivement, ``r.pk.film`` et ``r.pk.acteur``. Comme cette dernière notation peut paraître étrange, on peut ajouter le code suivant dans la classe ``Role`` qui implante un racourci vers le film et l'artiste. .. code-block:: java public Film getFilm() { return getPk().getFilm(); } public void setFilm(Film film) { getPk().setFilm(film); } public Artiste getActeur() { return getPk().getActeur(); } public void setActeur(Artiste acteur) { getPk().setActeur(acteur); } Maintenant, si ``r`` est une instance de ``Role``, ``r.film`` et ``r.acteur`` désignent respectivement le film et l'acteur. Il reste à regarder comment on code l'autre côté de l'association. Pour le film, cela donne: .. code-block:: java @OneToMany(mappedBy = "pk.film") private Set roles = new HashSet(); public Set getRoles() { return this.roles; } public void setRoles(Set r) { this.roles = r; } Seule subtilité: le ``mappedBy`` indique un *chemin* qui part de ``Role``, passe par la propriété ``pk`` de ``Role``, et arrive à la propriété ``film`` de ``Role.pk``. C'est là que l'on va trouver la définition du *mapping* avec la base, autrement le nom de la clé étrangère qui référence un film. Même chose du côté des artistes: .. code-block:: java @OneToMany(mappedBy = "pk.acteur") private Set roles = new HashSet(); public Set getRoles() { return this.roles; } public void setRoles(Set r) { this.roles = r; } .. _ex-jpamodel-composite: .. admonition:: Exercice: affichez tous les rôles Créez une action qui affiche tous les rôles, avec leur film et leur artiste. Nous avons fait un bon bout de chemin sur la compréhension du *mapping* JPA. Nous arrêtons là pour l'instant afin de bien digérer tout cela. Voici un exercice qui vous permettra de finir la mise en pratique. .. _ex-jpamodel-webscope: .. admonition:: Exercice: compléter le *mapping* de la base *webscope* Vous en savez maintenant assez pour compléter la description du *mapping* O/R de la base *webscope*. Une fois cela accompli, vous devriez pouvoir créer une action qui affiche tous les films avec leur metteur en scène, leurs acteurs et les rôles qu'ils ont joué. Vous devriez aussi pouvoir afficher les internautes, les notes qu'ils ont données et à quels films. ************************* Résumé: savoir et retenir ************************* À l'issue de ce chapitre,, assez dense, vous devez être en mesure de définir le *mapping* ORM pour la plupart des bases de données, une exception (rare) étant la représentation de *l'héritage*, pour lequel les bases relationnelles ne sont pas adaptées, mais que l'on va trouver potentiellement dans la modélisation objet de l'application. Il faut alors savoir produire une structure de base de données adaptée, et la *mapper* vers les classes de l'application. C'est ce que nous verrons dans le prochain chapitre. Il resterait encore des choses importantes à couvrir pour être complets sur le sujet. Entre autres: - les méthodes ``equals()`` et ``hashCode()`` sont importantes pour certaines classes; il faudrait les implanter; - des options d'annotation, dont celles qui indiquent comment *propager* la persistence en *cascade*, méritent d'être présentées. Concentrez-vous pour l'instant sur les connaissances acquise dans le présent chapitre, et gardez en mémoire que vous n'êtes pas encore complètement outillés pour faire face à des situations relativement marginales. Pour consolider vos acquis, il serait sans doute judicieux de prendre un schéma de base de données que vous connaissez et de produire le mapping adapté.