Participer au site avec un Tip
Rechercher
 

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 :

Mapping d'une relation @ManyToMany

Mapping d'une relation @OneToMany Mapping des collections



Accès rapide :
La vidéo
Introduction à la relation d'association de type « Many-To-Many »
Gestion des collections avec JPA
Mapping d'une relation @ManyToMany avec table d'association.
Implémentation, en base de données.
Implémentation du mapping JPA.
Test de la relation d'association.

La vidéo

Cette vidéo vous montre comment réaliser un mapping de relation de type @ManyToMany avec JPA (Java Persistence API).


Mapping JPA d'une relation d'association de type @ManyToMany

Introduction à la relation d'association de type « Many-To-Many »

Une association de type « Many-To-Many » et requise entre deux entités quand une instance du type de la première entité peut être mise en relation avec un nombre quelconque d'instance de second type et réciproquement. Par exemple, si l'on ajoute la notion de rôle à notre application, alors un utilisateur pourra avoir plusieurs rôle et un rôle pourra être associé à plusieurs utilisateurs. Le diagramme UML ci-dessous précise cet exemple.

Diagramme UML d'une relation « Many-To-Many »

Contrairement à ce que nous avions vu dans les chapitres précédents, il n'y a qu'une seule manière de réaliser une relation de type « Many-To-Many » en base de données : il faut impérativement passer par une table d'association.

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 ».

Gestion des collections avec JPA

Une relation de type « Many-To-Many » implique que nous allons récupérer des ensembles d'objets (to many). Il faut savoir que vous ne pouvez pas faire n'importe quoi à ce niveau. La règle la plus importante à garder à l'esprit et que vous devez obligatoirement typer les collections d'un mapping JPA par interfaces : si vous utiliser les classes d'implémentations (java.util.ArrayList, java.util.Vector...) vous produirez inexorablement une erreur d'exécution lors du chargement en mémoire des données du mapping.

Mapping d'une relation @ManyToMany avec table d'association.

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).

Implémentation, en base de données.

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_Roles ainsi qu'une nouvelle table d'association, appelée T_Users_Roles_Associations permettant de réaliser notre relation de type « Many-To-Many ».

-- ------------------------------------------------------------------------------
-- - Reconstruction de la base de données                                     ---
-- ------------------------------------------------------------------------------

DROP DATABASE IF EXISTS ManyToMany;
CREATE DATABASE ManyToMany;
USE ManyToMany;

-- -----------------------------------------------------------------------------
-- - 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 rôles                                      ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_Roles (
    idRole       int         PRIMARY KEY AUTO_INCREMENT,
    roleName     varchar(20) NOT NULL
);

INSERT INTO T_Roles (roleName)
VALUES ('client'), ('admin'), ('stockManager');

SELECT * FROM T_Roles;

-- -----------------------------------------------------------------------------
-- - Construction de la table d'association T_Users/T_Roles                  ---
-- -----------------------------------------------------------------------------

CREATE TABLE T_Users_Roles_Associations (
    idUser   int     NOT NULL REFERENCES T_Users(idUser),
    idRole   int     NOT NULL REFERENCES T_Roles(idRole)
);

INSERT INTO T_Users_Roles_Associations VALUES (1, 2), (1, 3), (4, 1), (5, 1);

SELECT * FROM T_Users_Roles_Associations;
Fichier Database.sql

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, 0 rows affected, 1 warning (0.005 sec)

Query OK, 1 row affected (0.001 sec)

Database changed
Query OK, 0 rows affected (0.036 sec)

Query OK, 5 rows affected (0.006 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.040 sec)

Query OK, 3 rows affected (0.005 sec)
Records: 3  Duplicates: 0  Warnings: 0

+--------+--------------+
| idRole | roleName     |
+--------+--------------+
|      1 | client       |
|      2 | admin        |
|      3 | stockManager |
+--------+--------------+
3 rows in set (0.000 sec)

Query OK, 0 rows affected (0.040 sec)

Query OK, 4 rows affected (0.005 sec)
Records: 4  Duplicates: 0  Warnings: 0

+--------+--------+
| idUser | idRole |
+--------+--------+
|      1 |      2 |
|      1 |      3 |
|      4 |      1 |
|      5 |      1 |
+--------+--------+
4 rows in set (0.000 sec)

MariaDB [ManyToMany]> 

dans notre exemple, l'utilisateur Anderson à deux rôles (admin et stockManager). Notez aussi que le rôle client est associé à deux utilisateurs (Ripley et Bond).

Implémentation du mapping JPA.

Pour réaliser une relation d'association de type « Many-To-Many » avec table d'association, il faut utiliser l'annotation @ManyToMany qu'il va falloir coupler à une annotation @JoinTable afin d'y spécifier les informations utiles à la table de 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_Users")
public class User {

    // Autres attributs
    
    @ManyToMany
    @JoinTable( name = "T_Users_Roles_Associations",
                joinColumns = @JoinColumn( name = "idUser" ),
                inverseJoinColumns = @JoinColumn( name = "idRole" ) )
    private List<Role> roles = new ArrayList<>();
            
    // Suite de la classe
    
}
Mise en oeuvre d'une relation de type @ManyToMany avec table d'association
comme nous l'avons dis, on n'oublie pas de typer la collection roles par interface, sans quoi une erreur sera produite.

Nous allons chercher à rendre cette relation « Many-To-Many » navigable dans les deux sens : permettre de retrouver les rôles d'un utilisateur et les utilisateurs associés à un rôle. Il va donc falloir placer une définition pour ce type de relation dans nos deux classes (User et Role). Voici le code complet de la classe User.

 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 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
package fr.koor.webstore.business;

import java.util.ArrayList;
import java.util.List;

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.ManyToMany;
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;
    
    @ManyToMany
    @JoinTable( name = "T_Users_Roles_Associations",
                joinColumns = @JoinColumn( name = "idUser" ),
                inverseJoinColumns = @JoinColumn( name = "idRole" ) )
    private List<Role> roles = new ArrayList<>();
            

    
    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 List<Role> getRoles() {
        return roles;
    }
    
    public String toString() {
        return this.idUser + ": " + this.login + "/" + 
               this.password + " - " + this.connectionNumber + " connexion(s)";
    }
    
}
La classe User et son mapping JPA

Et voici maintenant le code de la classe Role.

 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.ArrayList;
import java.util.List;

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.ManyToMany;
import javax.persistence.Table;

@Entity @Table(name="T_Roles")
public class Role {

    @Id @GeneratedValue(strategy=GenerationType.IDENTITY)
    private int idRole;
    private String roleName;
    
    @ManyToMany
    @JoinTable( name = "T_Users_Roles_Associations",
                joinColumns = @JoinColumn( name = "idRole" ),
                inverseJoinColumns = @JoinColumn( name = "idUser" ) )
    private List<User> users = new ArrayList<>();
            

    
    public Role() {
        this.idRole = 0;
        this.roleName = "unknown";
    }
    
    public int getIdRole() {
        return idRole;
    }
    
    public void setIdRole(int idRole) {
        this.idRole = idRole;
    }
    
    public String getRoleName() {
        return roleName;
    }
    
    public void setRoleName(String roleName) {
        this.roleName = roleName;
    }
    
    public List<User> getUsers() {
        return users;
    }
    
    @Override
    public String toString() {
        return "[" + this.roleName + "]";
    }
    
}
La classe Role et son mapping JPA

Test de la relation d'association.

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.Role</class> 
        <class>fr.koor.webstore.business.User</class> 
        
        <properties>
            <property name="javax.persistence.jdbc.driver" value="org.mariadb.jdbc.Driver" />
            <property name="javax.persistence.jdbc.url" value="jdbc:mariadb://localhost/ManyToMany" />
            <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>
Le fichier de configuration JPA/Hibernate : /META-INF/persistence.xml

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.

Voici maintenant un code de test permettant de valider que nous arrivons bien à exploiter notre relation « Many-To-Many » dans les deux sens.

 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 
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();
        
            User user = entityManager.find( User.class, 1 );            
            System.out.println( "Rôles associés à Anderson" );
            for( Role associatedRole : user.getRoles() ) {
                System.out.println( associatedRole );
            }

            Role role = entityManager.find( Role.class, 1 );            
            System.out.println( "Utilisateurs possédant le rôle client" );
            for( User associatedUser : role.getUsers() ) {
                System.out.println( associatedUser );
            }

        } finally {
            if ( entityManager != null ) entityManager.close();
            if ( entityManagerFactory != null ) entityManagerFactory.close();
        }

    }
}
Utilisation d'une relation de type @ManyToMany avec table d'association

Les résultats affichés devraient être les suivants.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
Rôles associés à Anderson
[admin]
[stockManager]
Utilisateurs possédant le rôle client
4: Ripley/Ellen - 0 connexion(s)
5: Bond/James - 0 connexion(s)


Mapping d'une relation @OneToMany Mapping des collections