AXOPEN

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.

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 idFournisseur;
    public static volatile SingularAttribute 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 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 qu’Hibernate 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 l’entité, il est impossible de mapper correctement une dépendance fonctionnelle de type @OneToMany vers cette entité.

Exemple : dans la classe Fournisseur, si l’on mappe une liste de FournisseurDepartement, on ne peut pas renseigner l’attribut mappedBy de l’annotation @OneToMany, puisqu’aucun 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 l’on 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 l’entité 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 s’agit d’une 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);
}