Mappage 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 propose plusieurs stratégies:

  • MappedSuperclass - les classes parentes ne peuvent pas être des entités
  • Table unique - les entités de différentes classes avec un ancêtre commun sont placées dans une seule table
  • Table jointe - chaque classe a sa table et l'interrogation d'une entité de sous-classe nécessite de joindre les tables
  • Table par classe - toutes les propriétés d'une classe sont dans sa table, donc aucune jointure n'est requise

Chaque stratégie aboutit à 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.

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

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

2. MappedSuperclass

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

Commençons par créer une classe Person qui représentera une classe parent:

@MappedSuperclass public class Person { @Id private long personId; private String name; // constructor, getters, setters }

Notez que cette classe n'a plus d' annotation @Entity , car elle ne sera pas conservée seule dans la base de données.

Ensuite, ajoutons une sous-classe Employee :

@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 unique

La stratégie Table unique crée une table pour chaque hiérarchie de classes. C'est aussi 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

Étant donné que les enregistrements de toutes les entités seront dans la même table, Hibernate a besoin d'un moyen de les différencier.

Par défaut, cela se fait via une colonne discriminante appelée DTYPE qui a le nom de l'entité comme valeur.

Pour personnaliser la colonne 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-classe MyProduct par une colonne entière appelée product_type .

Ensuite, nous devons indiquer à Hibernate la valeur de chaque enregistrement de sous-classe pour la colonne product_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 discriminante 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 { ... }

Cette stratégie présente l'avantage des performances de requête polymorphes, car une seule table doit être accédée lors de l'interrogation des entités parentes. D'un autre côté, cela signifie également que nous ne pouvons plus utiliser de contraintes NOT NULL sur les propriétés d'entité de sous-classe .

4. Table jointe

À l'aide de cette stratégie, chaque classe de la hiérarchie est mappée à sa 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 }

Both tables will have an animalId identifier column. The primary key of the Pet entity also has a foreign key constraint to the primary key of its parent entity. To customize this column, we can add the @PrimaryKeyJoinColumn annotation:

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

The disadvantage of this inheritance mapping method is that retrieving entities requires joins between tables, which can result in lower performance for large numbers of records.

The number of joins is higher when querying the parent class as it will join with every single related child – so performance is more likely to be affected the higher up the hierarchy we want to retrieve records.

5. Table per Class

The Table Per Class strategy maps each entity to its table which contains all the properties of the entity, including the ones inherited.

The resulting schema is similar to the one using @MappedSuperclass, but unlike it, table per class will indeed define entities for parent classes, allowing associations and polymorphic queries as a result.

To use this strategy, we only need to add the @Inheritance annotation to the base class:

@Entity @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) public class Vehicle { @Id private long vehicleId; private String manufacturer; // standard constructor, getters, setters }

Then, we can create the sub-classes in the standard way.

This is not very different from merely mapping each entity without inheritance. The distinction is apparent when querying the base class, which will return all the sub-class records as well by using a UNION statement in the background.

The use of UNION can also lead to inferior performance when choosing this strategy. Another issue is that we can no longer use identity key generation.

6. Polymorphic Queries

As mentioned, querying a base class will retrieve all the sub-class entities as well.

Let's see this behavior in action with a JUnit test:

@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); }

In this example, we've created two Book and Pen objects, then queried their super-class MyProduct to verify that we'll retrieve two objects.

Hibernate can also query interfaces or base classes which are not entities but are extended or implemented by entity classes. Let's see a JUnit test using our @MappedSuperclass example:

@Test public void givenSubclasses_whenQueryMappedSuperclass_thenOk() { MyEmployee emp = new MyEmployee(1, "john", "baeldung"); session.save(emp); assertThat(session.createQuery( "from com.baeldung.hibernate.pojo.inheritance.Person") .getResultList()) .hasSize(1); }

Notez que cela fonctionne également pour toute super-classe ou interface, qu'il s'agisse d'une @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 type EXPLICIT :

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

Dans ce cas, lors de la recherche d' articles, les enregistrements Bag ne seront pas retournés.

7. Conclusion

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

Le code source complet des exemples est disponible à l'adresse over sur GitHub.