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.

Exemple de relation OneToOne
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 ...
}
La classe associée dans la relation OneToOne
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 :

Déclaration de la clé étrangère
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 :

../_images/relation_one2one.png

Le modèle de base de données correspondant à la relation @OneToOne

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.

Exemple de relation ManyToOne
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 ...
}
La classe associée dans la relation ManyToOne
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.

../_images/relation_many2one.png

Le modèle de base de données correspondant à la relation @ManyToOne

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 :

Déclaration d’une table d’association
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 ...
}
../_images/relation_many2one_jointable.png

Le modèle de base de données correspondant à la relation @ManyToOne avec une table de jointure

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.

Exemple de relation OneToMany
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 ...
}
La classe associée dans la relation OneToMany
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.

../_images/relation_many2one.png

Le modèle de base de données correspondant à la relation @OneToMany

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.

Exemple de relation OneToMany avec une table de jointure
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 ...
}
../_images/relation_many2one_jointable.png

Le modèle de base de données correspondant à la relation @OneToMany avec une table de jointure

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.

Exemple de relation ManyToMany
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 ...
}
La classe associée dans la relation ManyToMany
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.

../_images/relation_many2many.png

Le modèle de base de données correspondant à la relation @ManyToMany

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.

Exemple de relation ManyToMany
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 ...
}
Exemple d’une relation bi-directionnelle avec mappedBy
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.

Exemple d’opérations en cascade
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.

Exemple d’utilisation de fetch lazy
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 :

../_images/relation_many2many_with_attribute.png

Le modèle de base de données avec un attribut sur la relation entre Individu et Competence

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 et Individu

  • une relation 1:N entre IndividuCompetence et Competence

La classe Competence reste identique à l’exemple vu précédemment :

La classe Competence
 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

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

La classe IndividuCompetencePK
 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).

La classe Individu
 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 :

La classe IndividuCompetencePK
 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.

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

../_images/relation_many2many_with_attribute_denormalized.png

Le modèle de base de données dénormalisé

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 :

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