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 des collections

Mapping d'une relation @ManyToMany Mapping d'une relation d'héritage



Accès rapide :
La vidéo
Les règles à connaître
On type les collections mappées avec JPA par des interfaces
Les interfaces autorisées
Mapping d'une collection basée sur l'interface java.util.List.
Mapping d'une collection basée sur l'interface java.util.Set.
Mapping d'une collection basée sur l'interface java.util.Map.

La vidéo

Cette vidéo vous montre comment mapper les collections à vos données en base.


Mapping JPA des collections

Les règles à connaître

Comme nous venons de le voir dans les deux derniers chapitres (mapping des relations @OneToMany et @ManyToMany), il est possible de mapper des collections. Dans les exemples précédents, nous avions fait simple : nos collections étaient typées par java.util.List (un ensemble séquentiel d'éléments).

JPA est plus subtile qui n'y parait et nous avons le contrôle sur le type de collections que nous pouvons utiliser. Mais attention, on ne peut pas faire n'importe quoi non plus.

On type les collections mappées avec JPA par des interfaces

Comme nous l'avons déjà dit, la règle la plus importante est qu'il faut obligatoirement typer nos collections par interfaces. Si on ne respecte pas cette règle, des erreurs seront produites.

A titre d'exemple et en repartant du code du chapitre précédent, j'ai modifié une relation de type « Many-To-Many » comme proposé dans l'exemple de code ci-dessous. Au lieu d'utiliser l'interface List j'ai utilisé la classe ArrayList.

 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 ArrayList<Role> roles = new ArrayList<>();
            
    // Suite de la classe
    
}
Erreur dans la définition du mapping d'une annotation @ManyToMany

Si on lance l'exemple proposé, voici l'erreur qui sera produite durant l'exécution du programme.

[main] WARN  | org.hibernate.orm.connections.pooling                        | HHH10001002: Using Hibernate built-in connection pool (not for production use!) (DriverManagerConnectionProviderImpl.java:70)
Exception in thread "main" org.hibernate.AnnotationException: Illegal attempt to map a non collection as a @OneToMany, @ManyToMany or @CollectionOfElements: fr.koor.webstore.business.User.roles
    at org.hibernate.cfg.annotations.CollectionBinder.getCollectionBinder(CollectionBinder.java:324)
    at org.hibernate.cfg.AnnotationBinder.processElementAnnotations(AnnotationBinder.java:1899)
    at org.hibernate.cfg.AnnotationBinder.processIdPropertiesIfNotAlready(AnnotationBinder.java:913)
    at org.hibernate.cfg.AnnotationBinder.bindClass(AnnotationBinder.java:740)
    at org.hibernate.boot.model.source.internal.annotations.AnnotationMetadataSourceProcessorImpl.processEntityHierarchies(AnnotationMetadataSourceProcessorImpl.java:249)
    at org.hibernate.boot.model.process.spi.MetadataBuildingProcess$1.processEntityHierarchies(MetadataBuildingProcess.java:222)
    at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:265)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:846)
    at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:873)
    at org.hibernate.jpa.HibernatePersistenceProvider.createEntityManagerFactory(HibernatePersistenceProvider.java:58)
    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:55)
    at javax.persistence.Persistence.createEntityManagerFactory(Persistence.java:39)
    at fr.koor.webstore.Console.main(Console.java:18)

Les interfaces autorisées

Deuxième règle : on ne peut pas utiliser n'importe quelle interface de collections. Les interfaces autorisées sont les suivantes :

le moteur Hibernate supporte aussi l'utilisation d'interface définie par le développeur : dans ce cas, l'interface devra étendre le type org.hibernate.usertype.UserCollectionType.

Pourquoi choisir telle ou telle interface ? La réponse est simple : ça dépend de ce que vous souhaitez faire avec vos données. Considérons l'exemple de code simple proposé ci-dessous : il est dérivé de l'exemple du chapitre précédent sur la mise en oeuvre d'une relation de type « Many-To-Many ». La différence, par rapport à l'exemple précédent, réside dans le fait que j'ai ajouté trois fois le même role admin à un l'utilisateur Anderson. Mais combien de fois allons nous remonter le rôle admin en mémoire quand nous chargerons l'instance Anderson ? Tout dépend de l'interface utilisée. Voici le code permettant de construire notre base de données de test.

-- ------------------------------------------------------------------------------
-- - 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, 2), (1, 2), (1, 3), (4, 1), (5, 1);

SELECT * FROM T_Users_Roles_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, 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, 6 rows affected (0.005 sec)
Records: 6  Duplicates: 0  Warnings: 0

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

MariaDB [ManyToMany]> 

Mapping d'une collection basée sur l'interface java.util.List.

Pour mapper une collection de type List sur des données en base, veuillez utiliser ce type de mapping. C'est clairement le code tel que nous l'avions présenté dans le chapitre précédent : je vous invite donc à le copier/coller, si ce n'est pas déjà fait, sur la page Web en question.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
@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
    
    public List<Role> getRoles() {
        return roles;
    }

    // Suite de la classe
    
}
Mapping d'une collection basée sur l'interface java.util.List

Si nous relançons le programme avec la nouvelle base de données présentée ci-dessus, voici les résultats que nous obtenons, à savoir trois fois le rôle admin associé à notre utilisateur.

[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]
[admin]
[admin]
[stockManager]

Pour rappel, voici le code de notre test de chargement d'un utilisateur et de la manipulation de ses rôles.

 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 
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 );
            }

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

    }
}
Code de test d'un mapping de collection.

Mapping d'une collection basée sur l'interface java.util.Set.

Le fait de choisir une collection de type java.util.Set va clairement changer les résultats produit. Un Set est une collection à valeur unique : si vous tentez d'insérer plusieurs fois une même valeur dans la collection, celle-ci ne sera comptabilisée qu'elle seule fois. Dans le cas d'un Set d'entités JPA, c'est la clé primaire des entités qui sera utilisée pour savoir si y a des redondances de données.

Pour changer la nature de la collection, rien ne doit changer au niveau des annotations du mapping. Par contre, il faut changer l'interface définie sur le champ correspondant à votre relation. Voici les modifications à apporter à votre classe User pour la baser sur une collection de type Set.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
@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 Set<Role> roles = new HashSet<>();
            
    // Suite de la classe
    
    public Set<Role> getRoles() {
        return roles;
    }

    // Suite de la classe
    
}
Mapping d'une collection basée sur l'interface java.util.Set

Prenez soin de relancer le programme et voici les résultats constatés : le rôle admin n'est présent qu'une seule et unique fois.

[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]
on aurez pu garantir l'unicité d'un rôle pour un utilisateur donné d'une autre manière. Effectivement, en rajoutant une contraire d'unicité sur un couple de clés de références Utilisateur/Role, vous garantirez aussi de ne pas avoir de doublons en base de données. Voici le code SQL de création de la table avec cette nouvelle contrainte.
-- -----------------------------------------------------------------------------
-- - 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),
    CONSTRAINT Users_Roles_Unicity UNIQUE (idUser, idRole)
);

Mapping d'une collection basée sur l'interface java.util.Map.

Il est aussi possible de stocker les données d'un mapping de collection dans une table associative clé/valeur. Vous pourrez ainsi retrouver les éléments de la collection par clé plutôt que par position ou séquentiellement. Pour ce faire, il faut sensiblement changer le mapping, pour y introduire l'interface java.util.Map, mais surtout, il faudra spécifier quel attribut de vos entités servira à fournir la valeur de la clé. Cela se fait via l'annotation @MapKey : dans notre cas, c'est la clé primaire qui servira de clé dans la map.

 1 
 2 
 3 
 4 
 5 
 6 
 7 
 8 
 9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
@Entity @Table(name = "T_Users")
public class User {

    // Autres attributs
    
    @ManyToMany 
    @MapKey( name = "idRole" )
    @JoinTable( name = "T_Users_Roles_Associations",
                joinColumns = @JoinColumn( name = "idUser" ),
                inverseJoinColumns = @JoinColumn( name = "idRole" ) )
    private Map<Integer, Role> roles = new HashMap<>();
            
    // Suite de la classe
    
    public Map<Integer, Role> getRoles() {
        return roles;
    }

    // Suite de la classe
    
}
Mapping d'une collection basée sur l'interface java.util.Map

Voici maintenant un exemple de code montrant comment utiliser la table associative des rôles portés par un l'utilisateur Anderson.

 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 
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().values() ) {
                System.out.println( associatedRole );
            }
            
            // Le role de clé primaire 2 est bien le rôle admin dans notre base de données.
            Role adminRole = user.getRoles().get(2);
            System.out.println( "Role 2 -> " + adminRole );

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

    }
}
Code de test d'un mapping de collection via l'interface java.util.Map.

Et voici les résultats produits par cet exemple de code.

[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]
Role 2 -> [admin]
une map garanti qu'elle ne stockera qu'une seule valeur pour une clé donnée. C'est ce qui explique que le rôle admin ne soit bien présent qu'une seule fois dans l'affichage ci-dessus.


Mapping d'une relation @ManyToMany Mapping d'une relation d'héritage