Cartographie d’héritage Hibernate

Cartographie d'héritage Hibernate

1. Vue d'ensemble

Les bases de données relationnelles ne disposent pas d'un moyen simple de mapper des hiérarchies de classes sur des tables de base de données.

Pour résoudre ce problème, la spécification JPA fournit plusieurs stratégies:

  • MappedSuperclass - les classes parentes ne peuvent pas être des entités

  • Single Table - les entités de différentes classes ayant un ancêtre commun sont placées dans une seule table

  • Table jointe - chaque classe a sa table et interroger une entité de sous-classe nécessite de rejoindre les tables

  • Table par classe - toutes les propriétés d'une classe sont dans sa table, aucune jointure n'est donc requise

Chaque stratégie résulte en une structure de base de données différente.

L'héritage d'entité signifie que nous pouvons utiliser des requêtes polymorphes pour récupérer toutes les entités de sous-classe lors d'une requête pour une super-classe.

Hibernate étant une implémentation JPA, il contient tout ce qui précède, ainsi que quelques fonctionnalités spécifiques à Hibernate relatives à l'héritage.

Dans les sections suivantes, nous examinerons plus en détail les stratégies disponibles.

2. MappedSuperclass

En utilisant la stratégieMappedSuperclass, l'héritage n'est évident que dans la classe, mais pas dans le modèle d'entité.

Commençons par créer une classePerson qui représentera une classe parente:

@MappedSuperclass
public class Person {

    @Id
    private long personId;
    private String name;

    // constructor, getters, setters
}

Notice that this class no longer has an @Entity annotation, car il ne sera pas conservé seul dans la base de données.

Ensuite, ajoutons une sous-classeEmployee:

@Entity
public class MyEmployee extends Person {
    private String company;
    // constructor, getters, setters
}

Dans la base de données, cela correspondra à une table“MyEmployee” avec trois colonnes pour les champs déclarés et hérités de la sous-classe.

Si nous utilisons cette stratégie, les ancêtres ne peuvent pas contenir d'associations avec d'autres entités.

3. Table simple

The Single Table strategy creates one table for each class hierarchy. C'est également la stratégie par défaut choisie par JPA si nous n'en spécifions pas explicitement.

Nous pouvons définir la stratégie que nous voulons utiliser en ajoutant l'annotation@Inheritance à la super-classe:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class MyProduct {
    @Id
    private long productId;
    private String name;

    // constructor, getters, setters
}

L'identifiant des entités est également défini dans la super-classe.

Ensuite, nous pouvons ajouter les entités de sous-classe:

@Entity
public class Book extends MyProduct {
    private String author;
}
@Entity
public class Pen extends MyProduct {
    private String color;
}

3.1. Valeurs discriminantes

Puisque les enregistrements de toutes les entités seront dans la même table,Hibernate needs a way to differentiate between them.

By default, this is done through a discriminator column called DTYPE qui a le nom de l'entité comme valeur.

Pour personnaliser la colonne du discriminateur, nous pouvons utiliser l'annotation@DiscriminatorColumn:

@Entity(name="products")
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name="product_type",
  discriminatorType = DiscriminatorType.INTEGER)
public class MyProduct {
    // ...
}

Ici, nous avons choisi de différencier les entités de sous-classeMyProduct par une colonneinteger appeléeproduct_type.

Ensuite, nous devons dire à Hibernate quelle valeur aura chaque enregistrement de sous-classe pour la colonneproduct_type:

@Entity
@DiscriminatorValue("1")
public class Book extends MyProduct {
    // ...
}
@Entity
@DiscriminatorValue("2")
public class Pen extends MyProduct {
    // ...
}

Hibernate ajoute deux autres valeurs prédéfinies que l'annotation peut prendre: «null» et «not null»:

  • @DiscriminatorValue(“null”) - signifie que toute ligne sans valeur de discriminateur sera mappée à la classe d'entité avec cette annotation; cela peut être appliqué à la classe racine de la hiérarchie

  • @DiscriminatorValue(“not null”) - toute ligne avec une valeur de discriminateur ne correspondant à aucune de celles associées aux définitions d'entité sera mappée à la classe avec cette annotation

Au lieu d'une colonne, nous pouvons également utiliser l'annotation@DiscriminatorFormula spécifique à Hibernate pour déterminer les valeurs de différenciation:

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorFormula("case when author is not null then 1 else 2 end")
public class MyProduct { ... }

This strategy has the advantage of polymorphic query performance since only one table needs to be accessed when querying parent entities. D'autre part, cela signifie également que les propriétés de l'entitéwe can no longer use NOT NULL constraints on sub-class.

4. Table jointe

Using this strategy, each class in the hierarchy is mapped to its table. La seule colonne qui apparaît à plusieurs reprises dans toutes les tables est l'identifiant, qui sera utilisé pour les joindre en cas de besoin.

Créons une super-classe qui utilise cette stratégie:

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Animal {
    @Id
    private long animalId;
    private String species;

    // constructor, getters, setters
}

Ensuite, nous pouvons simplement définir une sous-classe:

@Entity
public class Pet extends Animal {
    private String name;

    // constructor, getters, setters
}

Les deux tables auront une colonne d'identifiantanimalId. La clé primaire de l'entitéPet a également une contrainte de clé étrangère sur la clé primaire de son entité parente. Pour personnaliser cette colonne, nous pouvons ajouter l'annotation@PrimaryKeyJoinColumn:

@Entity
@PrimaryKeyJoinColumn(name = "petId")
public class Pet extends Animal {
    // ...
}

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, ce qui peut entraîner une baisse des performances pour un grand nombre d'enregistrements.

Le nombre de jointures est plus élevé lors de l'interrogation de la classe parente car celle-ci est associée à chaque enfant associé. Les performances risquent donc davantage d'être affectées par la hiérarchie supérieure que nous souhaitons récupérer.

5. Table par classe

La stratégie Table par classe mappe chaque entité à sa table qui contient toutes les propriétés de l'entité, y compris celles héritées.

Le schéma résultant est similaire à celui utilisant@MappedSuperclass, mais contrairement à lui, table par classe définira en effet des entités pour les classes parentes, permettant ainsi des associations et des requêtes polymorphes.

Pour utiliser cette stratégie, il suffit d'ajouter l'annotation@Inheritance à la classe de base:

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class Vehicle {
    @Id
    private long vehicleId;

    private String manufacturer;

    // standard constructor, getters, setters
}

Ensuite, nous pouvons créer les sous-classes de manière standard.

Ce n’est pas très différent de simplement mapper chaque entité sans héritage. La distinction est apparente lors de l'interrogation de la classe de base, qui renverra également tous les enregistrements de sous-classe en utilisant une instructionUNION en arrière-plan.

The use of UNION can also lead to inferior performance when choosing this strategy. Un autre problème est que nous ne pouvons plus utiliser la génération de clé d'identité.

6. Requêtes polymorphes

Comme mentionné, l'interrogation d'une classe de base récupérera également toutes les entités de sous-classe.

Voyons ce comportement en action avec un test JUnit:

@Test
public void givenSubclasses_whenQuerySuperclass_thenOk() {
    Book book = new Book(1, "1984", "George Orwell");
    session.save(book);
    Pen pen = new Pen(2, "my pen", "blue");
    session.save(pen);

    assertThat(session.createQuery("from MyProduct")
      .getResultList()).hasSize(2);
}

Dans cet exemple, nous avons créé deux objetsBook etPen, puis interrogé leurs super-classesMyProduct pour vérifier que nous allons récupérer deux objets.

Hibernate peut également interroger des interfaces ou des classes de base qui ne sont pas des entités mais qui sont étendues ou implémentées par des classes d'entités. Voyons un test JUnit en utilisant notre exemple@MappedSuperclass:

@Test
public void givenSubclasses_whenQueryMappedSuperclass_thenOk() {
    MyEmployee emp = new MyEmployee(1, "john", "example");
    session.save(emp);

    assertThat(session.createQuery(
      "from com.example.hibernate.pojo.inheritance.Person")
      .getResultList())
      .hasSize(1);
}

Notez que cela fonctionne également pour toute super-classe ou interface, que ce soit un@MappedSuperclass ou non. La différence avec une requête HQL habituelle est que nous devons utiliser le nom complet car ce ne sont pas des entités gérées par Hibernate.

Si nous ne voulons pas qu’une sous-classe soit renvoyée par ce type de requête, il suffit d’ajouter l’annotation Hibernate@Polymorphism à sa définition, de typeEXPLICIT:

@Entity
@Polymorphism(type = PolymorphismType.EXPLICIT)
public class Bag implements Item { ...}

Dans ce cas, lors de l'interrogation deItems,, les enregistrementsBag ne seront pas renvoyés.

7. Conclusion

Dans cet article, nous avons présenté les différentes stratégies de mappage de l'héritage dans Hibernate.

Le code source complet des exemples peut être trouvéover on GitHub.