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.
Cette vidéo vous montre comment mapper les collections à vos données en base.
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.
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 } |
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)
Deuxième règle : on ne peut pas utiliser n'importe quelle interface de collections. Les interfaces autorisées sont les suivantes :
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]>
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 } |
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(); } } } |
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 } |
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]
-- ----------------------------------------------------------------------------- -- - 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) );
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 } |
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(); } } } |
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]
admin
ne soit bien présent qu'une
seule fois dans l'affichage ci-dessus.
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 :