Hibernate 4 – Héritage – Mapping et stratégies

Dans une base de données relationnelle, il est souvent intéressant de faire de l’héritage. Mais comment peut-on représenter cet héritage avec Hibernate 4 ? Plusieurs stratégies existent, qui correspondent chacune à une représentation différente dans
Florent TRIPIERMis à jour le 14 Mars 2014
hibernate 4 mapping stratégie

Dans une base de données relationnelle, il est souvent intéressant de faire de lhéritage. Mais comment peut-on représenter cet héritage avec Hibernate 4 ? Plusieurs stratégies existent, qui correspondent chacune à une représentation différente dans le modèle de données.

1 Contexte

Pour détailler les différents mappings proposés par Hibernate, on prendra lexemple dune entité « employe » dont héritent deux entités : « technicien » et « ingenieur ». Les classes correspondantes sont Employe, Technicien et Ingenieur. Le modèle théorique dhéritage peut être représenté comme ci-dessous : 

joined.jpg

2 Lhéritage au sens strict

2.1 Principe

Le mode JOINED correspond au cas où super-entités et sous-entités sont chacunes représentées par une table en base de données, et sans réplication des champs communs. Ces champs sont uniquement portés par la super-table, et lid reporté dans les sous-tables permet de joindre pour récupérer les données de ces colonnes communes. Dans notre exemple, cela signifie que les champs « nom » et « prenom » sont uniquement portés par la table « employe » et que dans les tables « technicien » et « ingenieur », le champ « id » constitue une clé étrangère vers « employe » pour récupérer les « nom » et « prenom » à laide dune jointure.

Cela correspond donc au schéma théorique ci-dessus.

2.2 Mapping

Pour mettre en place ce mode dhéritage, il suffit dannoter la super-classe avec @Inheritance et le type JOINED. Cela définit le mode dhéritage pour toutes les sous-classes de cette super-classe.

@Entity
@Table(name = "employe")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class Employe implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
    @Column(name = "nom")
    private String nom;
    @Column(name = "prenom")
    private String prenom;

Comme ce mode dhéritage implique des jointures pour récupérer des instances des sous-classes, il faut préciser dans les classes filles sur quelle colonne seffectuera la jointure.

@Entity
@Table(name = "ingenieur")
@PrimaryKeyJoinColumn(name = "id")
public class Ingenieur extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "statut")
    private String statut;
    @Column(name = "nb_projets")
    private int nbProjets;
@Entity
@Table(name = "technicien")
@PrimaryKeyJoinColumn(name = "id")
public class Technicien extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "poste")
    private String poste;
    @Column(name = "niveau")
    private int niveau;

Ainsi, si je crée par exemple un Technicien et que je lenregistre en base de données avec la méthode persist de lEntityManager, Hibernate créera bien une ligne dans la table « employe » et une ligne dans la table « technicien » avec le même id.

De plus, notons que lon peut tout-à-fait utiliser le GenerationType.IDENTITY avec cette stratégie dhéritage (toutes les stratégies sont en fait supportées).

3 Un pseudo-héritage : dupliquer les données pour éviter les jointures 

3.1 Principe

Le mode TABLE_PER_CLASS correspond au cas où super-entités et sous-entités sont chacunes représentées par une table en base de données, et où chaque sous-entité réplique les champs de sa super-entité. Dans notre exemple, cela signifie que les tables « employe », « technicien » et « ingenieur » comportent toutes les champs « id », « nom » et « prenom ». 

table_per_classe.jpg

3.2 Mapping

Pour mettre en place cette stratégie, il suffit dannoter la super-classe avec @Inheritance et le type TABLE_PER_CLASS. Cela définit le mode dhéritage pour toutes les sous-classes de cette super-classe.

@Entity
@Table(name = "employe")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Employe implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @Column(name = "id")
    private int id;
    @Column(name = "nom")
    private String nom;
    @Column(name = "prenom")
    private String prenom;

Aucune autre information nest requise pour mapper les sous-classes.

@Entity
@Table(name = "ingenieur")
public class Ingenieur extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "statut")
    private String statut;
    @Column(name = "nb_projets")
    private int nbProjets;
@Entity
@Table(name = "technicien")
public class Technicien extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "poste")
    private String poste;
    @Column(name = "niveau")
    private int niveau;

Ici, si je crée par exemple un Technicien et que je lenregistre en base de données avec la méthode persist de lEntityManager, Hibernate ne créera quune ligne dans la table « technicien » et rien dans la table « employe ». Si je veux enregistrer aussi un Employe, je dois donc le faire explicitement.

De plus, notons quaucune stratégie de génération did nest ici spécifiée : en particulier, les GenerationType AUTO et IDENTITY ne sont pas supportés.

Remarque : les annotations @AttributeOverrides et @AttributeOverride permettent de surcharger le mapping des champs déclarés la super-classe :

@Entity
@Table(name = "technicien")
@AttributeOverrides({
    @AttributeOverride(name="nomDeFamille", column=@Column(name="nom")),
    @AttributeOverride(name="prenomUsuel", column=@Column(name="prenom"))
    })

public class Technicien extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "poste")
    private String poste;
    @Column(name = "niveau")
    private int niveau;

4 Une alternative intéressante : la table unique

4.1 Principe

Le mode SINGLE_TABLE correspond au cas où super-entités et sous-entités sont représentées par une seule table en tout et pour tout en base de données. Cette table contient donc tous les champs de la super-classe plus tous ceux de toutes les sous-classes. Ceci implique quil y ait des éléments NULL pour chaque tuple de cette table. De plus, une colonne sert de discriminant pour déterminer de quel type doit être tel ou tel tuple. Dans notre exemple, cela signifie quil ny a quune table « employe » qui comporte les champs « id », « nom » et « prenom », mais aussi « poste », « niveau », « statut » et « nb_projets », et enfin une colonne de discrimination (dont le nom par défaut est « DTYPE »). 

single_table.jpg

4.2 Mapping

Pour mettre en place ce mode dhéritage, il suffit dannoter la super-classe avec @Inheritance et le type SINGLE_TABLE. Cela définit le mode dhéritage pour toutes les sous-classes de cette super-classe.

@Entity
@Table(name = "employe")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public abstract class Employe implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
    @Column(name = "nom")
    private String nom;
    @Column(name = "prenom")
    private String prenom;

Aucune autre information nest requise pour mapper les sous-classes.

@Entity
@Table(name = "ingenieur")
public class Ingenieur extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "statut")
    private String statut;
    @Column(name = "nb_projets")
    private int nbProjets;
@Entity
@Table(name = "technicien")
public class Technicien extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "poste")
    private String poste;
    @Column(name = "niveau")
    private int niveau;

Dans cette stratégie, si je crée par exemple un Technicien et que je lenregistre en base de données avec la méthode persist de lEntityManager, Hibernate créera bien une ligne dans la table « employe » avec tous les champs des classes Employe et Technicien, plus le discriminant renseigné automatiquement avec la bonne valeur.

De plus, notons que, là encore, toutes les stratégies de génération did sont supportées.

Remarques : lannotation @DiscriminatorColumn dans la super-classe permet de surcharger le nom de la colonne du discriminant (valeur par défaut : « DTYPE »). Ci-dessous, on renomme cette colonne « discriminator » :

@Entity
@Table(name = "employe")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    name="discriminator",
    discriminatorType=DiscriminatorType.STRING
    )
public abstract class Employe implements Serializable {

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id")
    private int id;
    @Column(name = "nom")
    private String nom;
    @Column(name = "prenom")
    private String prenom;
}

De plus, lannotation @DiscriminatorValue aussi bien dans la super-classe que dans les sous-classes permet de surcharger la valeur du discriminant pour cette classe (valeur par défaut : le nom de la classe). Ci-dessous, on donne la valeur « ING » au discriminant pour le type Ingenieur :

@Entity
@Table(name = "ingenieur")
@DiscriminatorValue(value="ING")
public class Ingenieur extends Employe {

    private static final long serialVersionUID = 1L;

    @Column(name = "statut")
    private String statut;
    @Column(name = "nb_projets")
    private int nbProjets;

5 Conclusion

La meilleure stratégie a priori est JOINED : cest la seule implémentation de lhéritage au sens strict, tant en JavaLangage de développement très populaire ! que dans le modèle de données. Elle implique néanmoins systématiquement des jointures en lecture, et potentiellement plusieurs requêtes en écritures. Les requêtes de recherche peuvent vite devenir lourdes.

La stratégie SINGLE_TABLE est une alternative très honnête : les performances en lecture et écriture sont très bonnes. En revanche, on a potentiellement un grand nombre de colonnes et beaucoup de valeurs NULL.

Le mode TABLE_PER_CLASS est un peu le parent pauvre de ce comparatif avec ses allures dusine à gaz. Il est vrai que lon perd tout-à-fait lintérêt de lhéritage dans lapproche base de données (ce qui nest pas le cas du point de vue Java). On a potentiellement beaucoup de données dupliquées, ce qui signifie quune modification sur une des tables impliquées risque de devoir être répercutée sur une ou plusieurs autre(s) table(s). Il propose toutefois des performances correctes en lecture et en écriture (même sil faut gérer « à la main » la cohérence des données entre la super-classe et les sous-classes).