Logo Jetdev
Logo Jetdev
Edit Content

L’héritage avec Hibernate

Sur le projet Scanzee (https://scanzee.app), on souhaitait avoir une abstraction de l’entité vers laquelle renvoyait le scan d’un QR Code.
Or, le projet utilise une base PostgreSQL et nous souhaitions gérer ces entités un peu à la manière dont on l’aurait fait sur une base MongoDB: une collection qui accepterait tout type de question, un attribut « type » et on aurait pu ajouter tous les attributs nécessaires à chaque type sans trop de soucis. Cependant, les bases de données relationnelles n’ont pas un moyen simple de mapper les hiérarchies de classes sur des tables en base.
Pour remédier à ce problème, JPA nous fournit plusieurs stratégies et c’est ici que j’ai découvert le @Inheritance. Je vais vous présenter dans cet article les différentes manières de l’implémenter et les différentes structures de données que chaque stratégie induit.

Pour les exemples des différentes stratégies nous utiliserons une classe parent Vehicle qui aura 2 attributs: id et brand. Deux classes qui étendront Vehicle, qui seront Car et Bus, avec un attribut supplémentaire chacun à savoir numberOfHorses pour Car et numberOfSeats pour Bus (oui je n’étais pas très inspiré pour trouvé un bon cas d’exemple 😅).

1. MappedSuperClass

On annote avec @MappedSuperClass la classe parent, les sous-classes étendent donc la classe véhicule.

@MappedSuperClass
public class Vehicle {
    @Id
    private UUID id;
    private String brand;
}


@Entity
public class Car extends Vehicle {
    private Integer numberOfHorses;
}


@Entity
public class Bus extends Vehicle {
    private Integer numberOfSeats;
}

Avec le @MappedSuperClass l’héritage n’est visible que dans les classes Java. D’ailleurs la classe Vehicle n’est pas annotée avec @Entity. Côté base de données, les tables car et bus vont donc hériter des attributs de la classe Vehicle. Il n’y aura donc pas de table vehicle.

Pour une requête de type select, Hibernate va donc directement jouer son select sur la table de manière classique et ensuite mapper les données entre la classe Car/Bus et Vehicle.
Voici le select joué pour un findAll sur CarRepository:

select c1_0.id,c1_0.brand,c1_0.number_of_horses from public.car c1_0

2. Single Table

C’est la stratégie par défaut de JPA si on ne la spécifie pas.
Dans cette stratégie JPA va créer une seule table, représentée par la classe parent, avec les attributs des sous-classes.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
public class Vehicle {
    @Id
    private UUID id;
    private String brand;
}


@Entity
public class Car extends Vehicle {
    private Integer numberOfHorses;
}


@Entity
public class Bus extends Vehicle {
    private Integer numberOfSeats;
}

Côté base de données, la génération nous donne:

Vous pouvez noter l’apparition d’une colonne supplémentaire « dtype ». En effet, Hibernate a besoin de savoir de quel type sera l’objet en base. Par défaut, il ajoute une colonne « dtype » de type String qui sera le nom des sous-classes. Bien entendu on peut changer ce nom par défaut ainsi que le type, grâce au @DiscriminatorColumn et spécifier les valeurs souhaitées via @DiscriminatorValue.
Dans l’exemple suivant, on renomme le « dtype » en vehicle_type, on précise que le type sera un Integer avec une valeur à 1 pour les Car et 2 pour les Bus.

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


@Entity
@DiscriminatorValue("1")
public class Car extends Vehicle {
  //...
}


@Entity
@DiscriminatorValue("2")
public class Bus extends Vehicle {
  //...
}

On peut également préciser le type via une formule, par exemple:

@DiscriminatorFormula("case when number_of_seats is not null then 1 else 2 end")
public class Vehicle {
//...
}

Pour les requêtes en base, Hibernate va directement gérer avec ce « dtype » sur la table vehicle (je n’ai pas mis de DiscriminatorColumn pour ces exemples) :

// Find all cars
select c1_0.id,c1_0.brand,c1_0.number_of_horses from public.vehicle c1_0 where c1_0.dtype='Car'

// Find all bus
select b1_0.id,b1_0.brand,b1_0.number_of_seats from public.vehicle b1_0 where b1_0.dtype='Bus'

// Find all vehicles
select v1_0.id,v1_0.dtype,v1_0.brand,v1_0.number_of_seats,v1_0.number_of_horses from public.vehicle v1_0

3. Joined Table

Dans la stratégie Joined Table, chaque entité sera représentée par sa table. Les sous-classes hériteront seulement de l’id de la classe parent qui sera leur Primary Key également avec une contrainte de Foreign Key sur l’id de la classe parent.

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
public class Vehicle {
    @Id
    private UUID id;
    private String brand;
}


@Entity
public class Car extends Vehicle {
    private Integer numberOfHorses;
}


@Entity
public class Bus extends Vehicle {
    private Integer numberOfSeats;
}

Quand on génère le code des tables SQL pour voir la relation des Foreign Keys :

create table if not exists public.vehicle
(
    id    uuid not null
        primary key,
    brand varchar(255)
);

create table if not exists public.bus
(
    number_of_seats integer,
    id              uuid not null
        primary key
        constraint fk7uds9bih0ilsmyao2qy5ruhru
            references public.vehicle
);

create table if not exists public.car
(
    number_of_horses integer,
    id               uuid not null
        primary key
        constraint fkfugwdpykh9kb35q1quro44hrm
            references public.vehicle
);

Par défaut, les Primary Keys des sous-classes auront le même nom que l’id de la classe parent, ici « id ». Bien entendu Hibernate permet de customiser ce nom sur les sous-classes via l’annotation @PrimaryKeyJoinColumn.

@PrimaryKeyJoinColumn(name = "busId")
public class Bus extends Vehicle {
    private Integer numberOfSeats;
}

Le désavantage de cette stratégie est que toutes les tables sont liées. Hibernate va donc utiliser des join pour chaque requête. J’ai loggé les requêtes Hibernate pour vous montrer:

// Insert bus 
Hibernate: select b1_0.id,b1_1.brand,b1_0.number_of_seats from public.bus b1_0 join public.vehicle b1_1 on b1_0.id=b1_1.id where b1_0.id=?
Hibernate: insert into public.vehicle (brand,id) values (?,?)
Hibernate: insert into public.bus (number_of_seats,id) values (?,?)

// Find all bus
Hibernate: select b1_0.id,b1_1.brand,b1_0.number_of_seats from public.bus b1_0 join public.vehicle b1_1 on b1_0.id=b1_1.id

// Find all vehicles
Hibernate: select v1_0.id,case when v1_1.id is not null then 1 when v1_2.id is not null then 2 when v1_0.id is not null then 0 end,v1_0.brand,v1_1.number_of_seats,v1_2.number_of_horses from public.vehicle v1_0 left join public.bus v1_1 on v1_0.id=v1_1.id left join public.car v1_2 on v1_0.id=v1_2.id

Comme vous pouvez le constater, quand on souhaite récupérer un objet de type Vehicle, Hibernate n’a pas de moyen de savoir de quel sous-classe il s’agit, il va donc faire des join sur toutes les tables représentant les sous-classes. Plus vous en aurez, plus les performances en seront impactées.

4. Table per class

C’est la stratégie que nous avons adopté sur Scanzee. Ici, chaque entité a sa propre table avec ses attributs et les attributs hérités de la classe parent. La classe parent n’a donc pas de table si vous la mettez bien en « abstract » (sinon Hibernate génère une table également).

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Vehicle {
    @Id
    private UUID id;
    private String brand;
}


@Entity
public class Car extends Vehicle {
    private Integer numberOfHorses;
}


@Entity
public class Bus extends Vehicle {
    private Integer numberOfSeats;
}

Cette stratégie ressemble à la stratégie MappedSuperClass à la différence qu’ici les classes parent sont également des entités. Nous pouvons donc requêter directement sur un repository VehicleRepository.

// Insert bus
Hibernate: select b1_0.id,b1_0.brand,b1_0.number_of_seats from public.bus b1_0 where b1_0.id=?
Hibernate: insert into public.bus (brand,number_of_seats,id) values (?,?,?)

// Find all bus
Hibernate: select b1_0.id,b1_0.brand,b1_0.number_of_seats from public.bus b1_0

// Find all vehicles
Hibernate: select v1_0.id,v1_0.clazz_,v1_0.brand,v1_0.number_of_seats,v1_0.number_of_horses from (select id, brand, number_of_seats, null::integer as number_of_horses, 1 as clazz_ from public.bus union all select id, brand, null::integer as number_of_seats, number_of_horses, 2 as clazz_ from public.car) v1_0

On remarque donc que nous pouvons facilement requêter sur les tables représentant les sous-classes.
A contrario, quand nous lançons des requêtes sur la table parent, Hibernate utilise des UNION qui risquent également de détériorer vos performances applicatives.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *