Les clés primaires composées avec Hibernate 4

Dans une base de données relationnelle, la plupart des tables possèdent une clé primaire appliquée sur un seul champ. Cependant, une clé primaire peut s’appliquer à plusieurs d’entre eux : on parle de clé primaire composée.
Robin CHAUVINMis à jour le 28 Nov 2013

`` Dans une base de données relationnelle, la plupart des tables possèdent une clé primaire appliquée sur un seul champ. Cependant, une clé primaire peut s’appliquer à plusieurs d’entre eux : on parle de clé primaire composée.

1. Les prérequis

Pour comprendre cet article, il est recommandé de savoir faire un mapping Hibernate.

2. Structure de l’entité

L’entité présentera la particularité de posséder, à la place de l’annotation @Id habituelle, une Embedded key, grâce à l’annotation @EmbeddedId

Pour cet article, nous utiliserons le cas d’exemple suivant :

  • Une table FOURNISSEUR , enregistrant la liste des fournisseurs d’une société ;
  • Une table DEPARTEMENT, enregistrant la liste des départements (français) ;
  • Une table de jointure FOURNISSEUR_DEPARTEMENT; enregistrant les liens Fournisseur-Département, permettant de connaître la zone de livraison d’un fournisseur. Un champ supplémentaire VARCHAR informations_complementaires permettra de renseigner des données complémentaires sur la zone de livraison.

2.1. La classe de l’Embedded Key

La particularité de cette classe est de se voir attribuer l’annotation @Embeddable (exportable). Cette classe ne comportera que les champs de la clé primaire (dans notre exemple, il s’agira de ‘id_fournisseur’ et ‘id_departement’ de la table de jointure).

@Embeddable
public class FournisseurDepartementPK implements Serializable{
private static final long serialVersionUID = 1L;

    @Column(name="id_fournisseur")
    private int idFournisseur;

    @Column(name="id_departement")
    private int idDepartement;

    // Constructeur et getters/setters
}

Il est fortement conseillé de surcharger les méthodes equals() et hashcode() de cette classe.

Le metamodel associé est le même que d’habitude, à savoir

@StaticMetamodel(FournisseurDepartementPK.class)
public abstract class FournisseurDepartementPK_{

    public static volatile SingularAttribute<
FournisseurDepartementPK, Integer> idFournisseur;
    public static volatile SingularAttribute<
FournisseurDepartementPK, Integer> idDepartement;
}

2.2 L’entité

Comme précisé en introduction, le type de la clé primaire ne sera pas le même que d’habitude, et ses annotations seront également changées.

Pour rappel, voici comment se présente l’attribut dans une entité d’une table à clé primaire sur un seul champ.

@Entity
@Table(name = "fournisseur")
public class Fournisseur implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id_fournisseur")
    private int idFournisseur;

    // Constructeur et getters/setters
}

Et voici comment se présente l’entité qui nous concerne :

@Entity
@Table(name="fournisseur_departement")
public class FournisseurDepartement implements Serializable{
    private static final long serialVersionUID = 1L;

   @EmbeddedId
   private FournisseurDepartementPK id;

   // Constructeur, getters et setter
}

Le métamodèle, quand à lui, se présente ainsi :

@StaticMetamodel(FournisseurDepartement.class)

public abstract class FournisseurDepartement_{

    public static volatile SingularAttribute<
FournisseurDepartement, FournisseurDepartementPK> id;
}

3. Le lien avec les objets de la clé primaire

L’entité, dans sa forme actuelle, ne permet pas de faire le lien avec la classe Fournisseur ou la classe Département. Pourtant, cela est possible. voici deux méthodes pour le faire :

3.1. Attributs dans l’entité

La méthode la plus simple est d’ajouter les objets associés à la clé primaire directement dans l’entité, dans notre cas : Fournisseur et Departement.

Cependant, afin d’éviter quHibernate considère ces objets comme des entités à persister, il faut renseigner de nouveaux attributs dans les annotations.

Voici ce que cela donnerait :

@Entity
@Table(name = "fournisseur_departement")
public class FournisseurDepartement implements Serializable {
    private static final long serialVersionUID = 1L;

     @EmbeddedId
     private FournisseurDepartementPK id;

    //bi-directional many-to-one association to FournisseurDepartement
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "id_fournisseur", insertable = false, updatable = false)
     private Fournisseur fournisseur;

     // bi-directional many-to-one association to FournisseurDepartement
     @ManyToOne(fetch = FetchType.LAZY)
     @JoinColumn(name = "id_departement", insertable = false, updatable = false)
     private Departement departement;
            
     @Column(name = "informations_complementaires")
     private String informationsComplementaires;

     // Constructeur, getters et setters
 }

Grâce aux attributs ‘insertable’ et ‘updatable’ passés à false, Hibernate ne les persistera pas.

Ainsi, le fournisseur et le departement pourront être ajoutés normalement au métamodèle.

 

3.2 L’annotation @AttributeOverrides

Dans le cas où les attributs composant la clé primaire multiple ne sont pas surchargés dans lentité, il est impossible de mapper correctement une dépendance fonctionnelle de type @OneToMany vers cette entité.

Exemple : dans la classe Fournisseur, si lon mappe une liste de FournisseurDepartement, on ne peut pas renseigner lattribut mappedBy de lannotation @OneToMany, puisquaucun champ de la classe FournisseurDepartement ne correspond à notre cible :

@Entity
@Table(name = "fournisseur")
public class Fournisseur implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id_fournisseur")
    private int idFournisseur;

    @OneToMany(mappedBy = "???")
    private List fournisseurDepartements;

    // Constructeur et getters/setters
}

L’attibuteOverrides permet d’accéder au fournisseur et au département sans ajouter les objets concernés à l’entité FournisseurDepartement.

Voici comment cela se présente

@Entity
@Table(name = "fournisseur_departement")
public class FournisseurDepartement implements Serializable {
    private static final long serialVersionUID = 1L;

    @EmbeddedId
    @AttributeOverrides({
        @AttributeOverride(name = "id.fournisseur", column = @Column(name = "id_fournisseur")),
  @AttributeOverride(name = "id.departement", column = @Column(name = "id_departement")) })
    private FournisseurDepartementPK id;

    @Column(name = "informations_complementaires")
    private String informationsComplementaires;
    
    // Constructeur, getters et setters
}

L’annotation @AttributeOverrides permet de renseigner toutes les colonnes de la clé primaire. Chacune des colonnes est renseignée à l’aide d’une ligne d’annotation @AttributeOverride.

Comme on peut le voir, on retrouve, dans @AttributeOverride(name = « id.fournisseur », column = @Column(name = « id_fournisseur »)), le id.fournisseur, que lon va renseigner dans le @OneToMany de la classe Fournisseur.

@Entity
@Table(name = "fournisseur")
public class Fournisseur implements Serializable {
    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy=GenerationType.IDENTITY)
    @Column(name="id_fournisseur")
    private int idFournisseur;

    @OneToMany(mappedBy = "id.fournisseur")
    private List fournisseurDepartements;

    // Constructeur et getters/setters
}

Le fonctionnement est le même pour la classe Departement.

Grâce à cette méthode, il n’est plus nécessaire de rajouter les entités Fournisseur et Departement dans lentité FournisseurDepartement pour y avoir accès.

De même, il est important de noter que ces entités ne seront, de fait, pas à ajoutées au metamodel.

4. Récupérer une instance de l’entité par l’id (getById)

Habituellement, la méthode permettant de récupérer une entité par l’id utilise un objet de type primitif (la plupart du temps, numérique). Voici un rappel du fonctionnement :

public Fournisseur getFournisseurById(int pIdFournisseur) {
    return entityManager.find(Fournisseur.class, pIdFournisseur);
}

Dans notre cas, notre clé primaire n’est pas constituée d’un, mais de plusieurs champs. Cependant, ici, il sagit dune Embedded key, donc un objet.

Il suffit donc d’utiliser l’Embedded key. Voici comme cela se présente :

public FournisseurDepartement getFournisseurDepartementById(FournisseurDepartementPK pFournisseurDepartementPK) {
    return entityManager.find(FournisseurDepartement.class,pFournisseurDepartementPK);
}