Les relations avec JPA¶
Une des fonctionnalités majeures des ORM est de gérer les relations entre objets comme des relations entre tables dans un modèle de base de données relationnelle. JPA définit des modèles de relation qui peuvent être déclarés par annotation.
Les relations sont spécifiées par les annotations : @OneToOne, @ManyToOne, @OneToMany, @ManyToMany.
Pour une présentation approfondie des relations dans JPA, reportez-vous au Wikibook.
La relation 1:1 (one to one)¶
L’annotation @OneToOne définit une relation 1:1 entre deux entités. Si cette relation n’est pas forcément très courante dans un modèle relationnel de base de données, elle se rencontre très souvent dans une modèle objet.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToOne;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
private Abonnement abonnement;
// getters et setters omis ...
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Abonnement {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getters et setters omis ...
}
L’annotation @OneToOne implique que la table Individu
contient
une colonne qui est une clé étrangère contenant la clé d’un abonnement.
Par défaut, JPA s’attend à ce que cette colonne se nomme ABONNEMENT_ID
,
mais il est possible de changer ce nom grâce à l’annotation
@JoinColumn :
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne
@JoinColumn(name = "abonnement_fk")
private Abonnement abonnement;
// getters et setters omis ...
}
La déclaration de mapping ci-dessus correspond au modèle de base de données suivant :
Important
Dans une requête JPQL, une relation @OneToOne est navigable directement. Si on désire exprimer un requête de la forme
Sélectionner l’individu avec l’abonnement dont l’id est 1.
alors il suffit d’écrire en JPQL :
select i from Individu i where i.abonnement.id = 1
La relation n:1 (many to one)¶
L’annotation @ManyToOne définit une relation n:1 entre deux entités.
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
private Societe societe;
// getters et setters omis ...
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Societe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getters et setters omis ...
}
L’annotation @ManyToOne implique que la table Individu
contient
une colonne qui est une clé étrangère contenant la clé d’une société.
Par défaut, JPA s’attend à ce que cette colonne se nomme SOCIETE_ID
,
mais il est possible de changer ce nom grâce à l’annotation @JoinColumn.
Plutôt que par une colonne, il est également possible d’indiquer à JPA qu’il doit passer par une table d’association pour établir la relation entre les deux entités avec l’annotation @JoinTable :
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;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
// déclaration d'une table d'association
@JoinTable(name = "societe_individu",
joinColumns = @JoinColumn(name = "individu_id"),
inverseJoinColumns = @JoinColumn(name = "societe_id"))
private Societe societe;
// getters et setters omis ...
}
Important
Dans une requête JPQL, une relation @ManyToOne est navigable directement. Si on désire exprimer un requête de la forme
Sélectionner les individus appartenant à la société avec l’id 1.
alors il suffit d’écrire en JPQL :
select i from Individu i where i.societe.id = 1
La relation 1:n (one to many)¶
L’annotation @OneToMany définit une relation 1:n entre deux entités. Cette annotation ne peut être utilisée qu’avec une collection d’éléments puisqu’elle implique qu’il peut y avoir plusieurs entités associées.
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.OneToMany;
@Entity
public class Societe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany
private List<Individu> employes = new ArrayList<>();
// getters et setters omis ...
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getters et setters omis ...
}
L’annotation @OneToMany implique que la table Individu
contient
une colonne qui est une clé étrangère contenant la clé d’une société.
Par défaut, JPA s’attend à ce que cette colonne se nomme SOCIETE_ID
,
mais il est possible de changer ce nom grâce à l’annotation @JoinColumn.
Il est également possible d’indiquer à JPA qu’il doit passer par une table d’association pour établir la relation entre les deux entités avec l’annotation @JoinTable.
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.OneToMany;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
@Entity
public class Societe {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToMany
@JoinTable(name = "societe_individu",
joinColumns = @JoinColumn(name = "societe_id"),
inverseJoinColumns = @JoinColumn(name = "individu_id"))
private List<Individu> employes = new ArrayList<>();
// getters et setters omis ...
}
La relation @OneToMany est la relation miroir de la relation @ManyToOne. Le modèle relationnel de base de données est le même. Par contre, du point de vue du modèle objet, cela dépend du côté de la relation que l’on veut représenter. On parle de navigabilité de la relation. Dans l’exemple ci-dessus, le modèle objet est navigable d’une société vers ses individus.
Important
Dans une requête JPQL, une relation @OneToMany n’est pas navigable directement. Si on désire exprimer un requête de la forme
Sélectionner la société dont un des employés a l’id 1.
alors il faut utiliser en JPQL la syntaxe du fetch pour faire apparaître une variable intermédiaire dans la requête :
select s from Societe s join s.employes e where e.id = 1
La relation n:n (many to many)¶
L’annotation @ManyToMany définit une relation n:n entre deux entités. Cette annotation ne peut être utilisée qu’avec une collection d’éléments puisqu’elle implique qu’il peut y avoir plusieurs entités associées.
import java.util.List;
import java.util.ArrayList;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(name = "individu_competence",
joinColumns = @JoinColumn(name = "individu_id"),
inverseJoinColumns = @JoinColumn(name = "competence_id"))
private List<Competence> competences = new ArrayList<>();
// getters et setters omis ...
}
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Competence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getters et setters omis ...
}
L’annotation @ManyToMany implique qu’il existe une table d’association. On utilise l’annotation @JoinTable pour préciser le nom de la table d’association et le nom des colonnes contenant la clé pour l’individu et la clé pour la compétence.
Important
Dans une requête JPQL, une relation @ManyToMany n’est pas navigable directement. Si on désire exprimer un requête de la forme
Sélectionner les individus dont une des competences à l’id 1.
alors il faut utiliser en JPQL la syntaxe du fetch pour faire apparaître une variable intermédiaire dans la requête :
select i from Individu i join i.competences c where c.id = 1
Les relations bi-directionnelles¶
Parfois, il est nécessaire de construire un lien entre deux objets qui
soit navigables dans les deux sens. D’un point de vue objet, cela
signifie que chaque objet dispose d’un attribut pointant sur l’autre
instance. Mais d’un point de vue du schéma de base de données
relationnelle, il s’agit du même lien. Avec JPA, il est possible de
qualifier une relation entre deux objets comme étant une relation
inverse grâce à l’attribut mappedBy
de l’annotation indiquant le
type de relation.
import java.util.List;
import java.util.ArrayList;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany
@JoinTable(name = "individu_competence",
joinColumns = @JoinColumn(name = "individu_id"),
inverseJoinColumns = @JoinColumn(name = "competence_id"))
private List<Competence> competences = new ArrayList<>();
// getters et setters omis ...
}
import java.util.List;
import java.util.ArrayList;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;
@Entity
public class Competence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToMany(mappedBy = "competences")
private List<Individu> individus = new ArrayList<>();
// getters et setters omis ...
}
Dans l’exemple précédent, l’attribut individus
dans la classe Competence
correspond à la même relation que celle décrite par l’attribut competences
dans la classe Individu
. Ces deux attributs traduisent la même relation dans
le modèle relationnel de base de données. Il faut donc indiquer à JPA qu’il
existe un lien logique entre ces deux attributs. L’attribut mappedBy
de
l’annotation @ManyToMany permet d’indiquer le nom de l’attribut qui décrit
la même relation dans l’autre entité. Dans notre exemple, on déclare dans la
classe Competence
que l’attribut individus
correspond à l’attribut
competences
de la classe Individu
.
Vous n’avez aucune obligation à déclarer une relation bi-directionnelle et, si
vous le faites, il n’existe pas de règle pour décider lequel des deux attributs
doit être déclaré mappedBy
.
Par contre, vous ne devez pas utiliser mappedBy
dans la déclaration
des deux attributs mais uniquement pour celui qui est en quelque sorte le sens
secondaire de la relation. L’attribut mappedBy
est une façon de dire à JPA
que l’attribut ne représente pas le sens privilégié de la relation et que, s’il
désire obtenir des informations sur cette relation, il doit regarder la déclaration
de l’autre attribut. Il s’agit d’un moyen d’éviter de la duplication d’informations.
En effet, les annotations comme @JoinTable et @JoinColumn ne doivent être
ajoutées qu’à l’attribut qui n’a pas la déclaration mappedBy
.
Les relations bi-directionnelles ne sont pas limitées aux relations @ManyToMany. À une relation @ManyToOne peut correspondre une relation @OneToMany et à une relation @OneToOne peut correspondre une relation @OneToOne.
La propagation en cascade¶
Avec JPA, il est possible de propager des modifications à tout ou partie
des entités liées. Les annotations permettant de spécifier une relation
possèdent un attribut cascade
permettant de spécifier les opérations
concernées : ALL
, DETACH
, MERGE
, PERSIST
, REFRESH
ou
REMOVE
.
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(cascade = { CascadeType.PERSIST, CascadeType.MERGE })
private Societe societe;
// getters et setters omis ...
}
Dans l’exemple précédent, on précise que l’instance de société doit être enregistrée en base si nécessaire au moment où l’instance d’Individu sera elle-même enregistrée. De plus, lors d’un appel à merge pour une instance d’Individu, un merge sera automatiquement realisé sur l’instance de l’attribut société.
Requêtes JPQL et jointure¶
Dès que l’on introduit des relations entre entités, on complexifie également le langage de requêtage. Comment par exemple demander la liste des individus travaillant pour une société dont on passe le nom en paramètre ? On peut utiliser une jointure avec une condition :
select i from Individu i join i.societe s where s.nom = :nom
Il est également possible d’utiliser le mot-clé on
pour filtrer les entités
à sélectionner dans la jointure :
select i from Individu i join i.societe s on s.nom = :nom
Comme en SQL, JPQL fait la différence entre une jointure et une jointure externe (left outer join). Avec une jointure simple, on élimine toutes les entités pour lesquelles la jointure n’existe pas. Alors qu’avec une jointure externe, nous préservons les entités pour lesquelles il n’existe pas de jointure.
select i from Individu i left join i.societe s on s.nom = :nom
Attention avec l’exemple de requête ci-dessus : elle retourne l’union des individus qui
n’ont aucune relation avec l’entité société et ceux qui ont une relation avec
une société dont le nom est donné par le paramètre :nom
.
Fetch lazy ou fetch eager¶
Lorsque JPA doit charger une entité depuis la base de données (qu’il
s’agisse d’un appel à EntityManager.find(...)
ou d’une requête), la
question est de savoir quelles informations doivent être chargées.
Doit-il charger tous les attributs d’une entité ? Parmi ces attributs,
doit-il charger les entités qui sont en relation avec l’entité chargée ?
Ces questions sont importantes, car la façon d’y répondre peut avoir un
impact sur les performances de l’application.
Dans JPA, l’opération de chargement d’une entité depuis la base de
données est appelée fetch. Un fetch peut avoir deux stratégies :
eager ou lazy (que l’on peut traduire respectivement et
approximativement par chargement immédiat et chargement différé). On
peut décider de la stratégie pour chaque membre de la classe grâce à
l’attribut fetch
présent sur les annotations @Basic, @OneToOne,
@ManyToOne, @OneToMany et @ManyToMany.
eager signifie que l’information doit être chargée systématiquement lorsque l’entité est chargée. Cette stratégie est appliquée par défaut pour @Basic, @OneToOne et @ManyToOne.
lazy signifie que l’information ne sera chargée qu’à la demande (par exemple lorsque la méthode
get
de l’attribut sera appelée). Cette stratégie est appliquée par défaut pour @OneToMany et @ManyToMany.
import javax.persistence.Basic;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.ManyToOne;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Societe societe;
/*
* Stocke la photo d'identité sous une forme binaire. Comme l'information peut être
* volumineuse, on déclare un fetch lazy pour ne déclencher le chargement qu'à l'appel
* de getPhoto(), c'est-à-dire quand l'application en a vraiment besoin.
*/
@Lob
@Basic(fetch = FetchType.LAZY)
private byte[] photo;
// getters et setters omis ...
}
// Exécute une requête de la forme : SELECT i.id FROM Individu i WHERE i.id = ?
// Un appel à la méthode find ne charge pas les attributs marqués avec un fetch lazy.
Individu persistedIndividu = entityManager.find(Individu.class, individuId);
// Exécute une requête de la forme : SELECT i.photo FROM Individu i WHERE i.id = ?
byte[] photo = persistedIndividu.getPhoto();
// Exécute une requête de la forme : SELECT * FROM Societe WHERE id = ?
Societe societe = persistedIndividu.getSociete();
Il est possible d’utiliser une requête JPQL pour forcer la récupération d’informations en mode lazy si on sait qu’elle seront nécessaires plus tard dans le programme. Cela limite le nombre de requêtes à exécuter et peut se révéler nécessaire si la consultation des attributs lazy doit se faire à un moment où un entity manager n’est plus disponible. On utilise dans la requête l’option JOIN FETCH :
// Exécute une requête de type JOIN FETCH
String query = "select i from Individu i left join fetch i.societe where i.id = :id";
Individu persistedIndividu = entityManager.createQuery(query, Individu.class)
.setParameter("id", individuId)
.getSingleResult();
// N'exécute aucune requête car la société a déjà été récupérée par le left join fetch
Societe societe = persistedIndividu.getSociete();
La définition des stratégies de fetch est une partie importante du tuning dans le développement d’une application utilisant JPA.
Relation avec des attributs¶
Dans un modèle relationnel, il est courant que des tables d’association contiennent des colonnes pour caractériser une relation. Reprenons et complétons l’exemple de relation n:n présenté plus haut : un individu peut être associé à une ou plusieurs compétences et, pour chacune de ces compétences, il dispose d’un niveau de maîtrise (disons entre 0 et 5). Nous pouvons très facilement représenter cette relation avec le schéma suivant :
Cependant, dans le modèle objet, une relation entre deux objets ne peut pas avoir
d’attribut. Pour ce type de mapping, nous avons une différence importante
entre le modèle relationnel et le modèle objet. Si nous voulons traduire la colonne
niveau_maitrise
en Java, nous devons créer une entité pour représenter la table
individu_competence
. Ainsi, ce modèle ne peut pas être traduit comme une
relation n:n entre deux entités Individu
et Competence
mais comme deux
relations 1:n :
une relation 1:N entre
IndividuCompetence
etIndividu
une relation 1:N entre
IndividuCompetence
etCompetence
La classe Competence
reste identique à l’exemple vu précédemment :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Competence {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// getters et setters omis ...
}
|
Pour réaliser le reste du mapping, nous avons le choix entre trois solutions.
En effet, il existe plusieurs mappings pour représenter la clé composée de la
table individu_competence
.
Solution 1 : Référencer la clé composée avec @IdClass¶
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 | import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "individu_competence")
@IdClass(IndividuCompetencePK.class)
public class IndividuCompetence {
@Id
@Column(name = "individu_id")
private Long individuId;
@Id
@Column(name = "competence_id")
private Long competenceId;
@Column(name = "niveau_maitrise")
private int niveauMaitrise;
@ManyToOne
@JoinColumn(name = "individu_id", insertable = false, updatable = false)
private Individu individu;
@ManyToOne
@JoinColumn(name = "competence_id", insertable = false, updatable = false)
private Competence competence;
public int getNiveauMaitrise() {
return this.niveauMaitrise;
}
public void setNiveauMaitrise(int niveauMaitrise) {
this.niveauMaitrise = niveauMaitrise;
}
public Individu getIndividu() {
return this.individu;
}
public void setIndividu(Individu individu) {
this.individu = individu;
this.individuId = individu.getId();
}
public Competence getCompetence() {
return this.competence;
}
public void setCompetence(Competence competence) {
this.competence = competence;
this.competenceId = competence.getId();
}
}
|
La classe IndividuCompetence
est l’entité qui représente la table
individu_competence
, elle déclare deux relations @ManyToOne : à la ligne
25 pour la relation avec l’entité Individu
et à la ligne 29 pour la relation
avec l’entité Competence
. Elle dispose d’un attribut niveauMaitrise
afin
de fournir l’information sur la relation.
L’inconvénient est que cette entité dispose d’une clé primaire composée des deux
clés étrangères de l’association (individu_id
et competence_id
).
Nous sommes donc obligés de déclarer deux attributs avec l’annotation @Id pour
identifier les deux attributs qui forment la clé composée (lignes 14 et 18).
Remarquez que nous n’utilisons pas l’annotation @GeneratedValue pour ces
attributs. En effet, la clé ne doit pas être générée puisqu’elle est la composition
de deux autres clés déjà existantes : celle de Individu
et celle de Competence
.
Notre mapping associe deux fois les colonnes individu_id
et competence_id
à des attributs de la classe puisque ces deux colonnes sont à la fois des composantes
de la clé primaire et des clés étrangères. Nous sommes donc obligés d’indiquer à JPA
qu’il ne doit pas tenir compte de ces colonnes lorsqu’il doit faire des insertions
et des mises à jour pour les colonnes de jointure. C’est la signification
des attributs insertable = false
et updatable = false
pour les annotations
@JoinColumn aux lignes 26 et 30.
Plutôt que de fournir des getters/setters aux attributs individuId
et
competenceId
, nous préférons les mettre à jour lorsqu’on invoque les
méthodes setIndividu
(ligne 47) et setCompetence
(ligne 56). En effet,
ces identifiants sont en fait dupliqués puisqu’ils doivent être identiques à
la valeur des identifiants des objets de type Individu
et Competence
.
Une entité JPA doit forcément disposer d’une classe qui représente sa clé. Jusqu’à
présent, nous avons toujours déclaré des clés de type Long, une classe déjà
fournie par la bibliothèque standard Java. Dans le cas de l’entité
IndividuCompetence
, nous avons déclaré deux clés dans la classe. Il faut
donc fournir une classe supplémentaire pour représenter la composition de ces
deux clés. Grâce à l’annotation @IdClass, nous indiquons à la ligne 11 le type
de la classe Java qui représente la clé composée pour cette entité.
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 | import java.io.Serializable;
import java.util.Objects;
public class IndividuCompetencePK implements Serializable {
private Long individuId;
private Long competenceId;
public Long getIndividuId() {
return this.individuId;
}
public void setIndividuId(Long individuId) {
this.individuId = individuId;
}
public Long getCompetenceId() {
return this.competenceId;
}
public void setCompetenceId(Long competenceId) {
this.competenceId = competenceId;
}
public boolean equals(Object other) {
if (other instanceof IndividuCompetencePK) {
IndividuCompetencePK pk = (IndividuCompetencePK) other;
return Objects.equals(this.individuId, pk.individuId) &&
Objects.equals(this.competenceId, pk.competenceId);
}
return false;
}
public int hashCode() {
return Objects.hash(individuId, competenceId);
}
}
|
La classe IndividuCompetencePK
représente la clé de la classe
IndividuCompetence
. Son implémentation doit respecter les contraintes suivantes :
elle doit implémenter l’interface Serializable (ligne 4),
elle doit déclarer tous les attributs qui composent la clé primaire (lignes 6 et 8),
elle doit fournir une implémentation pour les méthodes equals (ligne 26) et hashCode (ligne 35).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | import java.util.ArrayList;
import java.util.List;
import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
@Entity
public class Individu {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@OneToMany(mappedBy="individu", cascade = CascadeType.ALL)
private List<IndividuCompetence> individuCompetences = new ArrayList<>();
// getters et setters omis ...
}
|
Pour notre exemple, il paraît utile qu’un objet de type Individu
possède une
liste de IndividuCompetence
pour permettre au programme de connaître le niveau
de compétence d’un individu donné. La relation entre Individu
et IndividuCompetence
est donc bi-directionnelle (ligne 18).
Notez que la relation avec la liste IndividuCompetence
indique une propagation
en cascade pour toutes les opérations. En effet, d’un point de vue objet, cette
relation est très certainement une relation de composition (aussi appelée
agrégation composite en UML) et dénote un lien très fort entre ces entités.
Solution 2 : Référencer la clé composée avec @EmbeddedId¶
L’implémentation précédente a un inconvénient : elle oblige à dupliquer les
attributs représentant la clé composée de l’entité IndividuCompetence
dans
la classe IndividuCompetencePK
qui représente la clé. Il est possible
d’utiliser la notion de clé embarquée pour éviter cette duplication. Le code Java
devient alors :
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 | import java.io.Serializable;
import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Embeddable;
@Embeddable
public class IndividuCompetencePK implements Serializable {
@Column(name = "individu_id")
private Long individuId;
@Column(name = "competence_id")
private Long competenceId;
public Long getIndividuId() {
return this.individuId;
}
public void setIndividuId(Long individuId) {
this.individuId = individuId;
}
public Long getCompetenceId() {
return this.competenceId;
}
public void setCompetenceId(Long competenceId) {
this.competenceId = competenceId;
}
public boolean equals(Object other) {
if (other instanceof IndividuCompetencePK) {
IndividuCompetencePK pk = (IndividuCompetencePK) other;
return Objects.equals(this.individuId, pk.individuId) && Objects.equals(this.competenceId, pk.competenceId);
}
return false;
}
public int hashCode() {
return Objects.hash(individuId, competenceId);
}
}
|
La classe IndividuCompetencePK
utilise l’annotation @Embeddable (ligne 9)
pour indiquer qu’elle peut être utilisée dans une entité pour fournir une partie
du mapping. Ainsi, elle déclare le mapping pour les attributs individuId
et
competenceId
.
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 | import javax.persistence.Column;
import javax.persistence.EmbeddedId;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "individu_competence")
public class IndividuCompetence {
@EmbeddedId
private IndividuCompetencePK id = new IndividuCompetencePK();
@Column(name = "niveau_maitrise")
private int niveauMaitrise;
@ManyToOne
@JoinColumn(name = "individu_id", insertable = false, updatable = false)
private Individu individu;
@ManyToOne
@JoinColumn(name = "competence_id", insertable = false, updatable = false)
private Competence competence;
public int getNiveauMaitrise() {
return this.niveauMaitrise;
}
public void setNiveauMaitrise(int niveauMaitrise) {
this.niveauMaitrise = niveauMaitrise;
}
public Individu getIndividu() {
return this.individu;
}
public void setIndividu(Individu individu) {
this.individu = individu;
this.id.setIndividuId(individu.getId());
}
public Competence getCompetence() {
return this.competence;
}
public void setCompetence(Competence competence) {
this.competence = competence;
this.id.setCompetenceId(competence.getId());
}
}
|
La classe IndividuCompetence
n’utilise plus l’annotation @IdClass et ne
déclare plus les différents attributs composant la clé. À la place, elle déclare
un attribut de type IndividuCompetencePK
avec l’annotation @EmbeddedId
(ligne 12) pour signaler à JPA que cet attribut fournit le mapping de la clé
composée. Le reste du code de la classe est adapté en fonction.
Solution 3 : Utiliser une clé technique simple¶
Il est important de comprendre que la complexité du mapping d’une table
d’association en Java vient du fait qu’un modèle relationnel et un modèle objet
ne se recoupent qu’imparfaitement. Dans les deux solutions vues précédemment, nous
adaptons le modèle objet pour qu’il corresponde au modèle relationnel. Mais nous
pouvons tout aussi bien adapter le modèle relationnel pour correspondre au modèle
objet. Pour cela, il suffit de dénormaliser le modèle relationnel en ajoutant
une colonne pour la clé primaire dans la table individu_competence
.
Dans ce cas, nous n’avons plus besoin d’une classe spécifique pour représenter
la clé et nous pouvons nous contenter de l’implémentation suivante pour la classe
IndividuCompetence
:
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 | import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
@Entity
@Table(name = "individu_competence")
public class IndividuCompetence {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Long id;
@Column(name = "niveau_maitrise")
private int niveauMaitrise;
@ManyToOne
@JoinColumn(name = "individu_id")
private Individu individu;
@ManyToOne
@JoinColumn(name = "competence_id")
private Competence competence;
// getters et setters omis ...
}
|