Accès rapide :
La vidéo
Introduction à la relation d'association de type « Many-To-One »
Mapping d'une relation @ManyToOne sans table d'association.
Implémentation, en base de données.
Implémentation du mapping JPA.
Test de la relation d'association.
Mapping d'une relation @ManyToOne avec table d'association.
Implémentation, en base de données.
Implémentation du mapping JPA.
Test de la relation d'association.
Cette vidéo vous présente les différentes techniques de mapping des relations de type @ManyToOne (avec ou sans table de jointures) proposées par JPA.
Nous allons voir, au travers de ce document, comment réaliser des associations de type « Many-To-One » avec JPA et Hibernate. Une relation de type « Many-To-One » a du sens quand plusieurs éléments peuvent être associés à un même élément : par exemple, plusieurs commandes peuvent être associées à une même personne. Si une telle relation est mise en oeuvre, il vous sera possible, à partir de la commande, de retrouver l'utilisateur associé.
Le diagramme UML ci-dessous vous montre l'association que nous allons chercher à mettre en oeuvre. La classe Command
représentera la commande
et la classe User
représentera la personne ayant passé la commande.
Mais attention : en base de données, vous avez deux manières de représenter une relation de type « Many-to-One ».
Mapping d'une relation @ManyToOne sans table d'association : dans ce cas, la clé de jointure, permettant la mise en relation, sera portée par la
table associée à la classe Command
. Nous appellerons cette table T_Commands
Mapping d'une relation @ManyToOne avec table d'association : dans ce cas, une troisième table (non associée à une entité JPA) servira à stocker les paires de clés de mise en relations.
Mettons en oeuvre chacune de ces possibilités en commençant par la première.
Voici le code SQL permettant de créer une base de données (pour le SGBDr MariaDB) avec deux tables mises en association par une relation de type « Many-To-One ».
-- ------------------------------------------------------------------------------ -- - Reconstruction de la base de données --- -- ------------------------------------------------------------------------------ DROP DATABASE IF EXISTS ManyToOne; CREATE DATABASE ManyToOne; USE ManyToOne; -- ----------------------------------------------------------------------------- -- - Construction de la table des utilisateurs --- -- ----------------------------------------------------------------------------- CREATE TABLE T_Users ( idUser int PRIMARY KEY AUTO_INCREMENT, login varchar(20) NOT NULL, password varchar(20) NOT NULL, connectionNumber int NOT NULL DEFAULT 0 ); INSERT INTO T_Users (login, password) VALUES ( 'Anderson', 'Neo' ), ( 'Skywalker', 'Luke' ), ( 'Plissken', 'Snake' ), ( 'Ripley', 'Ellen' ), ( 'Bond', 'James' ); SELECT * FROM T_Users; -- ----------------------------------------------------------------------------- -- - Construction de la table des commandes --- -- ----------------------------------------------------------------------------- CREATE TABLE T_Commands ( idCommand int PRIMARY KEY AUTO_INCREMENT, idUser int NOT NULL REFERENCES T_Users(IdUser), commandDate datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO T_Commands (idUser) VALUES (1), (2), (1); SELECT * FROM T_Commands;
Anderson
et une autre est associée à l'utilisateur Skywalker
.
Pour créer votre base de données, connectez-vous en mode ligne de commande à votre serveur puis exécutez l'ordre suivant :
MariaDB [(none)]> source Database.sql Query OK, 2 rows affected (0.066 sec) Query OK, 1 row affected (0.000 sec) Database changed Query OK, 0 rows affected (0.010 sec) Query OK, 5 rows affected (0.002 sec) Records: 5 Duplicates: 0 Warnings: 0 +--------+-----------+----------+------------------+ | idUser | login | password | connectionNumber | +--------+-----------+----------+------------------+ | 1 | Anderson | Neo | 0 | | 2 | Skywalker | Luke | 0 | | 3 | Plissken | Snake | 0 | | 4 | Ripley | Ellen | 0 | | 5 | Bond | James | 0 | +--------+-----------+----------+------------------+ 5 rows in set (0.000 sec) Query OK, 0 rows affected (0.013 sec) Query OK, 3 rows affected (0.002 sec) Records: 3 Duplicates: 0 Warnings: 0 +-----------+--------+---------------------+ | idCommand | idUser | commandDate | +-----------+--------+---------------------+ | 1 | 1 | 2020-01-21 11:42:01 | | 2 | 2 | 2020-01-21 11:42:01 | | 3 | 1 | 2020-01-21 11:42:01 | +-----------+--------+---------------------+ 3 rows in set (0.000 sec) MariaDB [ManyToOne]>
Il nous faut maintenant définir les deux classes d'entités associées à nos tables et y ajouter les annotations JPA de mapping.
En premier lieu, voici le code de la classe User
: c'est la classe la plus simple à coder, car ce n'est pas elle qui porte la relation
d'association.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package fr.koor.webstore.business; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; @Entity @Table(name = "T_Users") public class User { @Id @GeneratedValue( strategy=GenerationType.IDENTITY ) private int idUser; private String login; private String password; private int connectionNumber; public User() { } public User( String login, String password, int connectionNumber ) { super(); this.setLogin( login ); this.setPassword( password ); this.setConnectionNumber( connectionNumber ); } public int getIdUser() { return idUser; } public String getLogin() { return login; } public void setLogin(String login) { this.login = login; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public int getConnectionNumber() { return connectionNumber; } public void setConnectionNumber(int connectionNumber) { this.connectionNumber = connectionNumber; } public String toString() { return this.idUser + ": " + this.login + "/" + this.password + " - " + this.connectionNumber + " connexion(s)"; } } |
login
, password
et connectionNumber
ont les mêmes noms que leurs
colonnes associées en base de données, il n'est pas nécessaire de les marquer via des annotations JPA.
Maintenant il nous faut définir la classe Command
qui réalise la relation de type « Many-To-One » vers la classe User
.
Pour y mettre en place la relation, il va nous falloir utiliser l'annotation @ManyToOne
comme le montre cet extrait de code.
1 2 3 4 5 6 7 8 9 10 11 |
@Entity @Table(name = "T_Commands") public class Command { // Autres attributs @ManyToOne @JoinColumn( name="idUser" ) private User user; // Suite de la classe } |
Si vous le souhaitez, il est possible de cascader certaines actions initiées sur une instance de la classe Command
à l'instance de type
User
qui lui est associée. Les actions pouvant être cascadées sont : DETACH
, MERGE
, PERSIST
REFRESH
et REMOVE
. Par exemple, si l'on cascade le REMOVE
, une suppression d'une instance de commande
entraînera une suppression automatiquement de l'utilisateur associée. Nous reviendrons sur ces possibilités ultérieurement. En utilisant la constante
CascadeType.ALL
on demande à cascader toutes ces actions.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Entity @Table(name = "T_Commands") public class Command { // Autres attributs @ManyToOne( cascade = CascadeType.ALL ) @JoinColumn( name="idUser" ) private User user; // Suite de la classe } |
Il est aussi possible d'imposer la présence d'une instance de type User
pour chaque commande en fixant l'attribut
nullable
à false
sur l'annotation @JoinColumn
.
1 2 3 4 5 6 7 8 9 10 11 12 |
@Entity @Table(name = "T_Commands") public class Command { // Autres attributs @ManyToOne @JoinColumn( name="idUser", nullable=false ) private User user; // Suite de la classe } |
Voici le code complet de la classe Command
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
package fr.koor.webstore.business; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.Table; @Entity @Table(name="T_Commands") public class Command { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int idCommand; @ManyToOne @JoinColumn(name="idUser", nullable=false) private User user; private Date commandDate; public Command() {} public Command( User user, Date commandDate ) { this.setUser( user ); this.setCommandDate( commandDate ); } public int getIdCommand() { return idCommand; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Date getCommandDate() { return commandDate; } public void setCommandDate(Date commandDate) { this.commandDate = commandDate; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append( "Commande de >> " ).append( this.user ) .append( " - " ).append( this.commandDate ).append( "\n" ); return builder.toString(); } } |
Pour configurer le projet JPA, il est nécessaire de fournir le fichier META-INF/persistence.xml
.
N'oubliez pas qu'il doit être accessible à partir de CLASSPATH
. En voici sa définition.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="WebStore"> <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider> <class>fr.koor.webstore.business.User</class> <class>fr.koor.webstore.business.Command</class> <properties> <property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mariadb://localhost/ManyToOne" /> <property name="javax.persistence.jdbc.user" value="root" /> <property name="javax.persistence.jdbc.password" value="" /> <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLDialect" /> <property name="hibernate.format_sql" value="false" /> </properties> </persistence-unit> </persistence> |
Il vous faut, bien entendu un fichier de configuration pour Log4J (du moins, si vous souhaitez l'utiliser) : je vous renvoie à ce sujet sur un des chapitres précédents pour de plus amples informations.
Nous allons commencer par charger une instance de commande par sa clé primaire en invoquant la méthode entityManager.find
.
Nous descendons ensuite dans l'instance représentant l'utilisateur ayant passé la commande en invoquant la méthode command.getUser()
.
Normalement, JPA doit se débrouiller de charger les instances nécessaires de manière automatique (grâce au mapping).
Voici l'exemple de code associé à ce scénario.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
package fr.koor.webstore; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import fr.koor.webstore.business.Command; import fr.koor.webstore.business.User; public class Console { public static void main(String[] args) throws Exception { EntityManagerFactory entityManagerFactory = null; EntityManager entityManager = null; try { entityManagerFactory = Persistence.createEntityManagerFactory("WebStore"); entityManager = entityManagerFactory.createEntityManager(); Command command = entityManager.find( Command.class, 1 ); System.out.println( command ); User user = command.getUser(); System.out.println( user ); } finally { if ( entityManager != null ) entityManager.close(); if ( entityManagerFactory != null ) entityManagerFactory.close(); } } } |
Voici les résultats produits par cet exemple.
[main] WARN | org.hibernate.orm.connections.pooling | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70) Commande de >> 1: Anderson/Neo - 0 connexion(s) - 2020-01-21 11:42:01.0 1: Anderson/Neo - 0 connexion(s)
Pour rappel, le terme de table d'association est aussi parfois appelé table de jointure. Il s'agit d'une table en base de données permettant d'associer deux enregistrements situés dans deux autres tables de la base de données en utilisant des « foreign keys » (des clés étrangères en français).
Parfois, des tables en base de données sont déjà existantes, mais aucune relation ne les relie. Vous pourriez simplement ajouter une clé de référence dans l'une des deux tables pour établir la relation. Mais si d'autres applications utilisent déjà ces tables, l'ajout d'une nouvelle colonne peut poser soucis. Dans ce cas, l'ajout d'une table d'association vous permet de gérer votre problème d'association sans impacter les applications existantes. Le contre coup étant qu'il faut réaliser plus de jointures pour extraire vos données (c'est un peu moins performant).
Voici le code SQL permettant de créer une base de données (pour le SGBDr MariaDB) avec nos deux tables T_Users
et
T_Commands
ainsi qu'une nouvelle table d'association, appelée T_Commands_Users_Associations
permettant de réaliser notre relation de type « Many-To-One ».
-- ------------------------------------------------------------------------------ -- - Reconstruction de la base de données --- -- ------------------------------------------------------------------------------ DROP DATABASE IF EXISTS ManyToOne; CREATE DATABASE ManyToOne; USE ManyToOne; -- ----------------------------------------------------------------------------- -- - Construction de la table des utilisateurs --- -- ----------------------------------------------------------------------------- CREATE TABLE T_Users ( idUser int PRIMARY KEY AUTO_INCREMENT, login varchar(20) NOT NULL, password varchar(20) NOT NULL, connectionNumber int NOT NULL DEFAULT 0 ); INSERT INTO T_Users (login, password) VALUES ( 'Anderson', 'Neo' ), ( 'Skywalker', 'Luke' ), ( 'Plissken', 'Snake' ), ( 'Ripley', 'Ellen' ), ( 'Bond', 'James' ); SELECT * FROM T_Users; -- ----------------------------------------------------------------------------- -- - Construction de la table des commandes --- -- ----------------------------------------------------------------------------- CREATE TABLE T_Commands ( idCommand int PRIMARY KEY AUTO_INCREMENT, commandDate datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ); INSERT INTO T_Commands () VALUES (), (), (); SELECT * FROM T_Commands; -- ----------------------------------------------------------------------------- -- - Construction de la table d'association T_Commands/T_Users --- -- ----------------------------------------------------------------------------- CREATE TABLE T_Commands_Users_Associations ( IdCommand int NOT NULL REFERENCES T_Commands(IdCommand), IdUser int NOT NULL UNIQUE REFERENCES T_Users(IdUser) ); INSERT INTO T_Commands_Users_Associations VALUES (1, 1), (2, 2), (3, 1); SELECT * FROM T_Commands_Users_Associations;
Pour construire la base de données, veuillez de nouveau exécuter l'ordre source Database.sql
dans la console de MariaDB.
Voici les résultats produits.
MariaDB [(none)]> source Database.sql Query OK, 3 rows affected (0.075 sec) Query OK, 1 row affected (0.000 sec) Database changed Query OK, 0 rows affected (0.012 sec) Query OK, 5 rows affected (0.002 sec) Records: 5 Duplicates: 0 Warnings: 0 +--------+-----------+----------+------------------+ | idUser | login | password | connectionNumber | +--------+-----------+----------+------------------+ | 1 | Anderson | Neo | 0 | | 2 | Skywalker | Luke | 0 | | 3 | Plissken | Snake | 0 | | 4 | Ripley | Ellen | 0 | | 5 | Bond | James | 0 | +--------+-----------+----------+------------------+ 5 rows in set (0.000 sec) Query OK, 0 rows affected (0.014 sec) Query OK, 3 rows affected (0.002 sec) Records: 3 Duplicates: 0 Warnings: 0 +-----------+---------------------+ | idCommand | commandDate | +-----------+---------------------+ | 1 | 2020-01-21 12:35:06 | | 2 | 2020-01-21 12:35:06 | | 3 | 2020-01-21 12:35:06 | +-----------+---------------------+ 3 rows in set (0.000 sec) Query OK, 0 rows affected (0.033 sec) Query OK, 3 rows affected (0.006 sec) Records: 3 Duplicates: 0 Warnings: 0 +-----------+--------+ | IdCommand | IdUser | +-----------+--------+ | 1 | 1 | | 2 | 2 | | 3 | 1 | +-----------+--------+ 3 rows in set (0.000 sec) MariaDB [ManyToOne]>
Rien ne change pour la classe User
. Laissez-la telle qu'elle.
Pour réaliser une relation d'association de type « Many-To-One » avec table d'association, il faut encore utiliser l'annotation
@ManyToOne
, mais il va falloir la coupler à une annotation @JoinTable
afin d'y spécifier les informations utiles à la
jointure. Voici un extrait de code relatif à la définition d'une telle relation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
@Entity @Table(name = "T_Commands") public class Command { // Autres attributs @ManyToOne @JoinTable( name = "T_Commands_Users_Associations", joinColumns = @JoinColumn( name = "idCommand" ), inverseJoinColumns = @JoinColumn( name = "idUser" ) ) private User user; // Suite de la classe } |
Voici le code complet de la classe Command
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
package fr.koor.webstore.business; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.JoinTable; import javax.persistence.ManyToOne; import javax.persistence.Table; @Entity @Table(name="T_Commands") public class Command { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) private int idCommand; @ManyToOne @JoinTable( name = "T_Commands_Users_Associations", joinColumns = @JoinColumn( name = "idCommand" ), inverseJoinColumns = @JoinColumn( name = "idUser" ) ) private User user; private Date commandDate; public Command() {} public Command( User user, Date commandDate ) { this.setUser( user ); this.setCommandDate( commandDate ); } public int getIdCommand() { return idCommand; } public User getUser() { return user; } public void setUser(User user) { this.user = user; } public Date getCommandDate() { return commandDate; } public void setCommandDate(Date commandDate) { this.commandDate = commandDate; } public String toString() { StringBuilder builder = new StringBuilder(); builder.append( "Commande de >> " ).append( this.user ) .append( " - " ).append( this.commandDate ).append( "\n" ); return builder.toString(); } } |
Normalement, vous pouvez récupérer le fichier de configuration JPA proposé plus haut dans ce document. Aucune modification ne devrait être nécessaire, sauf si vous avez changé le nom de la base de données.
Vous devriez aussi pouvoir reprendre les exemples d'utilisation de vos classes précédemment proposés et ils devraient correctement fonctionner. Pour rappel, voici un petit exemple d'utilisation.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package fr.koor.webstore; import javax.persistence.EntityManager; import javax.persistence.EntityManagerFactory; import javax.persistence.Persistence; import fr.koor.webstore.business.Command; import fr.koor.webstore.business.User; public class Console { public static void main(String[] args) throws Exception { EntityManagerFactory entityManagerFactory = null; EntityManager entityManager = null; try { entityManagerFactory = Persistence.createEntityManagerFactory("WebStore"); entityManager = entityManagerFactory.createEntityManager(); Command command = entityManager.find( Command.class, 1 ); System.out.println( command ); User user = command.getUser(); System.out.println( user ); } finally { if ( entityManager != null ) entityManager.close(); if ( entityManagerFactory != null ) entityManagerFactory.close(); } } } |
Les résultats affichés devraient rester inchangés.
[main] WARN | org.hibernate.orm.connections.pooling | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70) Commande de >> 1: Anderson/Neo - 0 connexion(s) - 2020-01-21 12:39:55.0 1: Anderson/Neo - 0 connexion(s)
Améliorations / Corrections
Vous avez des améliorations (ou des corrections) à proposer pour ce document : je vous remerçie par avance de m'en faire part, cela m'aide à améliorer le site.
Emplacement :
Description des améliorations :