AXOPEN

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 le modèle de données.

1 – Contexte

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

joined

2 – L’hé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 l’id 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 » à l’aide d’une jointure.

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

2.2 – Mapping

Pour mettre en place ce mode d’héritage, il suffit d’annoter la super-classe avec @Inheritance et le type JOINED. Cela définit le mode d’hé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 d’héritage implique des jointures pour récupérer des instances des sous-classes, il faut préciser dans les classes filles sur quelle colonne s’effectuera 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 l’enregistre en base de données avec la méthode persist de l’EntityManager, 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 l’on peut tout-à-fait utiliser le GenerationType.IDENTITY avec cette stratégie d’hé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

3.2 – Mapping

Pour mettre en place cette stratégie, il suffit d’annoter la super-classe avec @Inheritance et le type TABLE_PER_CLASS. Cela définit le mode d’hé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 n’est 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 l’enregistre en base de données avec la méthode persist de l’EntityManager, Hibernate ne créera qu’une 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 qu’aucune stratégie de génération d’id n’est 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 qu’il 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 qu’il n’y a qu’une 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

4.2 – Mapping

Pour mettre en place ce mode d’héritage, il suffit d’annoter la super-classe avec @Inheritance et le type SINGLE_TABLE. Cela définit le mode d’hé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 n’est 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 l’enregistre en base de données avec la méthode persist de l’EntityManager, 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 d’id sont supportées.

Remarques : l’annotation @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, l’annotation @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 : c’est la seule implémentation de l’héritage au sens strict, tant en Java 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 d’usine à gaz. Il est vrai que l’on perd tout-à-fait l’intérêt de l’héritage dans l’approche base de données (ce qui n’est pas le cas du point de vue Java). On a potentiellement beaucoup de données dupliquées, ce qui signifie qu’une 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 s’il faut gérer « à la main » la cohérence des données entre la super-classe et les sous-classes).