Intéressé par des cours d'informatique en ligne ?
Visitez mon nouveau site https://www.yesik.it !

Une des différences fondamentales entre le modèle objet et le modèle relationnel est que ce dernier n'est pas capable de gérer la notion d'héritage. C'est un obstacle majeur lorsqu'il s'agit de faire persister un modèle objet complexe dans une base de données relationnelle. Heureusement, JPA – l'interface standard Java pour la persistance – est capable de venir à notre secours dans ce domaine également. C'est ce que nous allons voir dans cet article.

La bonne compréhension de cette article nécessite d'avoir déjà eu un premier contact avec JPA. Si ce n'est pas le cas, je vous engage à lire d'abord l'article Premier contact avec JPA et Hibernate.

Le projet initial

Pour cet article, nous allons utiliser comme support un exemple simple et original. Histoire de marquer votre esprit. Donc voici: le département marketing d'un grand éditeur de romans a déterminé qu'il y avait un marché potentiel important pour des romans d'Heroic Fantasy écrit par ... un logiciel.

Un des points clés du projet est que les personnages doivent pouvoir revenir d'un roman à un autre: les lecteurs aiment bien les personnages récurrents et les éditeurs adorent les sagas. Bref, étant chargé du projet, vous décidez d'utiliser JPA/Hibernate pour pouvoir enregistrer les personnages dans une base de données relationnelle.

A ce stade du projet, vous en êtes arrivé à la définition de l'entité suivante pour représenter un personnage dans l'application:

package fr.chicoree.jpa.heroes;
 
import javax.persistence.*;
 
@Entity
class Personnage {
    public static enum Comportement { HOSTILE, AMICAL };
 
    // Requis par JPA
    private Personnage() {}
 
    public Personnage(String nom, Comportement comportement) {
        this.nom = nom;
        this.comportement = comportement;
    }
 
    public String getNom() {
        return nom;
    }
 
    public String toString() {
        return nom;
    }
 
    @Id
    private String nom;
    private Comportement comportement;
}

Quand à la configuration de JPA la voici:

<persistence>
    <persistence-unit name="jpa-heroes-pu">
        <!-- Définit Hibernate comme fournisseur de persistance -->
        <provider>org.hibernate.ejb.HibernatePersistence</provider>
 
        <properties>
            <property name="hibernate.archive.autodetection" value="class" />
            <!-- <property name="hibernate.show_sql" value="true" /> -->
            <property name="hibernate.format_sql" value="true" />
 
            <!-- Configuration de la  BDD -->
            <property name="hibernate.connection.driver_class" value="com.mysql.jdbc.Driver" />
            <property name="hibernate.connection.url" value="jdbc:mysql://localhost/jpa-heroes" />
            <property name="hibernate.connection.username" value="jpa-user" />
            <property name="hibernate.connection.password" value="SomePassword" />
 
            <!-- Spécifie le 'dialecte' SQL utilisé pour communiquer avec la BDD -->
            <property name="hibernate.dialect" value="org.hibernate.dialect.MySQLInnoDBDialect" />
 
            <!-- Indique a Hibernate de (re-)créer la BDD au lancement de l'application -->
            <property name="hibernate.hbm2ddl.auto" value="create" />
        </properties>
 
    </persistence-unit>
</persistence>

La base de données ayant été créée sous MySQL ainsi:

CREATE DATABASE `jpa-heroes`;
GRANT ALL TO `jpa-heroes`.* TO 'jpa-user'@localhost IDENTIFIED BY "SomePassword";

Et enfin, voici le programme qui permet d'ajouter et afficher un personnage. Le code est assez long, mais facile à comprendre. Vous remarquerez que j'ai bien distingué l'ajout de l'affichage en créant deux méthodes séparées. Par soucis d'exhaustivité en voici le code complet:

package fr.chicoree.jpa.heroes;
 
import javax.persistence.*;
import java.util.*;
 
public class DemoPersonnage {
    private EntityManager   em;
 
    public DemoPersonnage(EntityManager em) {
        this.em = em;
    }
 
    /**
     * Ajoute des personnages.
     *
     * Pour plus de simplicité, les personnages à ajouter sont codés
     * en dur dans ce programme de démonstration.
     */
    public void ajoute() {
        Personnage  personnages[] = {
            new Personnage("Bilbo", Personnage.Comportement.AMICAL),
            new Personnage("Smaug", Personnage.Comportement.HOSTILE),
            new Personnage("Thorin", Personnage.Comportement.AMICAL),
            new Personnage("Gloin", Personnage.Comportement.AMICAL)
        };
 
        // Commence une transaction
        EntityTransaction tx = em.getTransaction();
        tx.begin();
 
        // Rend les entités persistantes
        for(Personnage p : personnages) {
            em.persist(p);
        }
 
        // Applique les modifications à la base de données
        tx.commit();
    }
 
    /**
     * Affiche tous les personnages de la base de données
     */
    public void affiche() {
        // Inutile de créer une transaction ici: on ne modifie pas la base de données
        Query query = em.createQuery("SELECT p FROM Personnage p ORDER BY nom ASC");
 
        @SuppressWarnings("unchecked")
        List<Personnage> personnages = (List<Personnage>)query.getResultList();
        for(Personnage p : personnages) {
            System.out.printf("%10s [%s]%n", p, p.getClass().getName());
        }
    }
 
    public static void main(String[] args) {
        // Démarre JPA et ouvre une session
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpa-heroes-pu");
        EntityManager em = emf.createEntityManager();
 
        /*
         * Code spécifique
         */
        DemoPersonnage prog = new DemoPersonnage(em);
        prog.ajoute();
        prog.affiche();
 
        // Ferme la session et termine JPA
        em.close();
        emf.close();
    }
}

Une fois Hibernate installé et configuré, l'arborescence de votre projet devrait ressembler à ceci:

sh$ find .
.
./lib
./lib/hibernate-core.jar
./lib/jta.jar
./lib/log4j.jar
./lib/asm-attrs.jar
./lib/javassist.jar
./lib/slf4j-api.jar
./lib/junit.jar
./lib/dom4j.jar
./lib/cglib.jar
./lib/hibernate-annotations.jar
./lib/ejb3-persistence.jar
./lib/asm.jar
./lib/mysql-connector-java.jar
./lib/commons-collections.jar
./lib/hibernate-commons-annotations.jar
./lib/hibernate-entitymanager.jar
./lib/antlr.jar
./lib/slf4j-log4j12.jar
./fr
./fr/chicoree
./fr/chicoree/jpa
./fr/chicoree/jpa/heroes
./fr/chicoree/jpa/heroes/DemoPersonnage.java
./fr/chicoree/jpa/heroes/Personnage.java
./log4j.properties
./META-INF
./META-INF/persistence.xml

Note:

Si vous venez d'installer des bibliothèques, pensez à mettre à jour le CLASSPATH!

Une fois compilé vous pouvez "jouer" avec le programme:

sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
    Bilbo [fr.chicoree.jpa.heroes.Personnage]
    Gloin [fr.chicoree.jpa.heroes.Personnage]
    Smaug [fr.chicoree.jpa.heroes.Personnage]
   Thorin [fr.chicoree.jpa.heroes.Personnage]

Entités polymorphiques

Ajout d'un comportement

Pour l'instant, nos personnages sont plutôt passifs. Mais pour avoir un roman à succès il est impératif que ceux-ci soient un tant soit peu plus dynamiques. Et dans un roman d'aventure, une des action favorite des personnages est d'attaquer leurs ennemis. Nous allons mettre en oeuvre cette action sous la forme d'une méthode à ajouter à l'entité Personnage:

@Entity
class Personnage {
/* ... */
    public String attaque() {
        return String.format("%s passe à l'attaque!", nom);
    }
/* ... */
}

Rien d'extraordinaire ici – mais modifions quand même le programme de démonstration pour voir apparaître le résultat:

public class DemoPersonnage {
/* ... */
    /**
     * Affiche tous les personnages de la base de données
     */
    public void affiche() {
        // Inutile de créer une transaction ici: on ne modifie pas la base de données
        Query query = em.createQuery("SELECT p FROM Personnage p ORDER BY nom ASC");
 
        @SuppressWarnings("unchecked")
        List<Personnage> personnages = (List<Personnage>)query.getResultList();
        for(Personnage p : personnages) {
            System.out.printf("%10s [%s]: %s%n",
                                p, p.getClass().getName(),
                                p.attaque()); // Affiche l'attaque préférée du personnage
        }
    }
/* ... */
}

La seule modification du programme de test est l'affichage de l'attaque favorite du personnage. Ce qui nous donne:

sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Personnage]: Gloin passe à l'attaque!
     Smaug [fr.chicoree.jpa.heroes.Personnage]: Smaug passe à l'attaque!
    Thorin [fr.chicoree.jpa.heroes.Personnage]: Thorin passe à l'attaque!

Au passage, cela confirme que c'est bien un objet à part entière qui est "récupéré" par Hibernate. Avec ses données, mais aussi ses méthodes.

A ce stade, il est peut-être profitable de vérifier ce qui se trouve dans la base de données:

mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Personnage           |
+----------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN Personnage;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Comme vous le voyez, Hibernate a bien créé la table pour les entités Personnage. Vérifions ce qui est enregistré dedans:

mysql> SELECT * FROM Personnage;
+--------+--------------+
| nom    | comportement |
+--------+--------------+
| Bilbo  |            1 |
| Gloin  |            1 |
| Smaug  |            0 |
| Thorin |            1 |
+--------+--------------+
4 rows in set (0.00 sec)

Normalement, vous êtes en territoire connu. Il est donc temps de s'aventurer vers des contrées quelques peu plus hostiles!

Un peu de diversité

Un roman où tous les personnages agissent de manière identique risque d'être bien vite lassant. Qui plus est, il serait souhaitable que les personnages agissent de façon un peu stéréotypée: les dragons crachent le feu, les nains brandissent des haches, etc. Tout ceci appelle à pleine voix l'utilisation de l'héritage et du polymorphisme. Or donc, voici deux nouvelles entités: le Dragon et le Nain. Mais ne mettons pas la charrue avant les boeufs et commençons tout d'abord avec la classe fr.chicoree.jpa.heroes.Dragon:

package fr.chicoree.jpa.heroes;
 
class Dragon extends Personnage {
    public Dragon(String nom, Comportement comportement) {
        super(nom, comportement);
    }
 
    public String attaque() {
        return String.format("%s crache le feu", getNom());
    }
}

Et modifions de manière adéquate le programme de démonstration pour bien créer un dragon pour le personnage de Smaug:

public class DemoPersonnage {
/* ... */
    /**
     * Ajoute des personnages.
     *
     * Pour plus de simplicité, les personnages à ajouter sont codés
     * en dur dans ce programme de démonstration.
     */
    public void ajoute() {
        Personnage  personnages[] = {
            new Personnage("Bilbo", Personnage.Comportement.AMICAL),
            new Dragon("Smaug", Personnage.Comportement.HOSTILE),  // Smaug est un Dragon !!!
            new Personnage("Thorin", Personnage.Comportement.AMICAL),
            new Personnage("Gloin", Personnage.Comportement.AMICAL)
        };
 
        /* ... */
}

La compilation ne pose pas de problème. L'exécution par contre...

sh$ javac fr/chicoree/jpa/heroes/*.java
sh$  java fr.chicoree.jpa.heroes.DemoPersonnage
Exception in thread "main" java.lang.IllegalArgumentException: Unknown entity: fr.chicoree.jpa.heroes.Dragon
        at org.hibernate.ejb.AbstractEntityManagerImpl.persist(AbstractEntityManagerImpl.java:223)
        at fr.chicoree.jpa.heroes.DemoPersonnage.ajoute(DemoPersonnage.java:33)
        at fr.chicoree.jpa.heroes.DemoPersonnage.main(DemoPersonnage.java:65)

Effectivement, il y a eu un problème lorsque l'on a tenté de rendre persistant notre dragon. En effet, la classe Java Dragon n'est pour l'instant pas encore une entité JPA. Autrement dit, j'ai juste oublié l'annotation @Entity:

package fr.chicoree.jpa.heroes;
 
import javax.persistence.*;
 
@Entity
class Dragon extends Personnage {
    public Dragon(String nom, Comportement comportement) {
        super(nom, comportement);
    }
 
    public String attaque() {
        return String.format("%s crache le feu", getNom());
    }
}
sh$ javac fr/chicoree/jpa/heroes/*.java
sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Personnage]: Gloin passe à l'attaque!
     Smaug [fr.chicoree.jpa.heroes.Dragon]: Smaug crache le feu
    Thorin [fr.chicoree.jpa.heroes.Personnage]: Thorin passe à l'attaque!

Inutile d'insister sur ce résultat: Hibernate a fait ce qu'on attendait de lui en rechargeant "Smaug" comme un Dragon et non comme un Personnage. Par quel miracle? La solution est dans la base de données:

mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Personnage           |
+----------------------+
1 row in set (0.00 sec) 

mysql> EXPLAIN Personnage;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| DTYPE        | varchar(31)  | NO   |     |         |       |
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

Toujours une seule table – mais une nouvelle colonne est apparue: DTYPE. Il n'y a pas besoin d'être très malin pour supposer que c'est là qu'Hibernate stocke l'information qui lui permet de différencier les enregistrements correspondants à des instances de la classe Dragon de celles de la classe Personnage. Un SELECT va nous le confirmer:

mysql> SELECT * FROM Personnage;
+------------+--------+--------------+
| DTYPE      | nom    | comportement |
+------------+--------+--------------+
| Personnage | Bilbo  |            1 |
| Personnage | Gloin  |            1 |
| Dragon     | Smaug  |            0 |
| Personnage | Thorin |            1 |
+------------+--------+--------------+
4 rows in set (0.00 sec)

Voilà le mystère levé: Hibernate mémorise tout simplement le nom de l'entité dans la table. Mais, et si les classes dérivées avaient leurs propres données? Où seraient-elles sauvegardées? Voici poindre quelques nains qui vont nous aider à répondre à cette question.

Tous pareils, mais tous différents

Peut-être l'ignorez-vous, mais les nains sont très fiers de leurs ancêtres. Et c'est faire preuve de bonne manière que de se présenter en précisant un ou plusieurs de ses ascendants. Ici nous nous limiterons pour chaque nain au nom de son père. Ce qui nous donne l'entité suivante:

package fr.chicoree.jpa.heroes;
 
import javax.persistence.*;
 
@Entity
class Nain extends Personnage {
    public Nain(String nom, String parent, Comportement comportement) {
        super(nom, comportement);
 
        this.parent = parent;
    }
 
    public String attaque() {
        return String.format("%s fils de %s brandit son arme", getNom(), parent);
    }
 
    private String parent;
}

Et comme tout à l'heure, il faut aussi mettre à jour le programme de démonstration:

public class DemoPersonnage {
/* ... */
    /**
     * Ajoute des personnages.
     *
     * Pour plus de simplicité, les personnages à ajouter sont codés
     * en dur dans ce programme de démonstration.
     */
    public void ajoute() {
        Personnage  personnages[] = {
            new Personnage("Bilbo", Personnage.Comportement.AMICAL),
            new Dragon("Smaug", Personnage.Comportement.HOSTILE),
            new Nain("Thorin", "Thrain", Personnage.Comportement.AMICAL), // Thorin, fils de Thrain
            new Nain("Gloin", "Groin", Personnage.Comportement.AMICAL) // Gloin, fils de Groin
        };
 
        /* ... */
}

Le moment de vérité:

sh$ javac fr/chicoree/jpa/heroes/*.java
sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Nain]: Gloin fils de Groin brandit son arme
     Smaug [fr.chicoree.jpa.heroes.Dragon]: Smaug crache le feu
    Thorin [fr.chicoree.jpa.heroes.Nain]: Thorin fils de Thrain brandit son arme

Où donc Hibernate a-t-il sauvegardé les ascendants de chacun de nos nains? A nouveau, un examen de la base de données s'impose:

mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Personnage           |
+----------------------+
1 row in set (0.00 sec)

mysql> EXPLAIN Personnage;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| DTYPE        | varchar(31)  | NO   |     |         |       |
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
| parent       | varchar(255) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
4 rows in set (0.00 sec)

Toujours une seule table! Vraisemblablement Hibernate synthétise tous les champs de toutes les classes de l'arborescence des entités dans une seule table correspondant à la classe de base. A nouveau, l'examen des données ne recèle aucune mauvaise surprise:

mysql> SELECT * FROM Personnage;
+------------+--------+--------------+--------+
| DTYPE      | nom    | comportement | parent |
+------------+--------+--------------+--------+
| Personnage | Bilbo  |            1 | NULL   |
| Nain       | Gloin  |            1 | Groin  |
| Dragon     | Smaug  |            0 | NULL   |
| Nain       | Thorin |            1 | Thrain |
+------------+--------+--------------+--------+
4 rows in set (0.00 sec)

Si cette stratégie peut vous sembler curieuse, après réflexion elle explique comment Hibernate est capable efficacement de recharger des instances de différentes classes à partir d'une seule requête JPQL:

        Query query = em.createQuery("SELECT p FROM Personnage p ORDER BY nom ASC");

Malgré tout, si l'arborescence est un tant soit peu complexe, cette stratégie ne risque-t-elle pas d'être source d'inefficacité? Conscient de ce problème JPA propose de choisir une stratégie alternative pour gérer l'héritage dans une base de données relationnelle. C'est ce que nous allons examiner pour terminer.

Stratégies alternatives

SINGLE_TABLE

Nous avons vu que par défaut, Hibernate choisit pour stratégie une table unique pour stocker tous les champs de toutes les classes d'une arborescence d'entités. Dans le vocabulaire de JPA, c'est la stratégie InheritanceType.SINGLE_TABLE. Vous pouvez l'indiquer explicitement dans le code de l'entité à la racine de l'arborescence avec l'annotation @Inheritance:

/* ... */
 
@Entity
@Inheritance(strategy=InheritanceType.SINGLE_TABLE)
class Personnage {
    /* ... */
}

Après avoir testé, vous pouvez vous convaincre que le programme fonctionne toujours de manière identique et qu'il n'y a toujours qu'une seule table:

sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Nain]: Gloin fils de Groin brandit son arme
     Smaug [fr.chicoree.jpa.heroes.Dragon]: Smaug crache le feu
    Thorin [fr.chicoree.jpa.heroes.Nain]: Thorin fils de Thrain brandit son arme
mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Personnage           |
+----------------------+
1 row in set (0.00 sec)

Bref, rien n'a changé. Avant de passer à des stratégies plus originales pour vous, posons nous tout de même la question de savoir ce que doit faire Hibernate pour répondre à certaines requêtes.

Note:

L'objectif est d'illustrer le fonctionnement d'Hibernate face a diverses stratégies pour gérer l'héritage. Pas de donner des détails de mise en oeuvre d'Hibernate.

Si vous voulez voir la requête réellement utilisée dans votre programme, vous pouvez le faire en ajoutant l'option suivante à la fin de votre fichier log4j.properties:

log4j.logger.org.hibernate.SQL=DEBUG

Par exemple, considérons la requête JPQL suivante qui récupère toutes les instances de la classe Personnage et de ses classes dérivées:

SELECT p FROM Personnage p

Dans la stratégie SINGLE_TABLE cela devient en SQL:

mysql> SELECT DTYPE, nom, comportement, parent FROM Personnage;
+------------+--------+--------------+--------+
| DTYPE      | nom    | comportement | parent |
+------------+--------+--------------+--------+
| Personnage | Bilbo  |            1 | NULL   |
| Nain       | Gloin  |            1 | Groin  |
| Dragon     | Smaug  |            0 | NULL   |
| Nain       | Thorin |            1 | Thrain |
+------------+--------+--------------+--------+
4 rows in set (0.00 sec)

Par contre si l'on ne s'intéressait qu'aux instances de la classe Nain la requête JPQL serait:

SELECT p FROM Nain

Ce qui se traduirait en SQL par:

mysql> SELECT nom, comportement, parent FROM Personnage WHERE DTYPE = 'Nain';
+--------+--------------+--------+
| nom    | comportement | parent |
+--------+--------------+--------+
| Gloin  |            1 | Groin  |
| Thorin |            1 | Thrain |
+--------+--------------+--------+
2 rows in set (0.00 sec)

Comme vous le constatez, en terme de requête SQL, la stratégie SINGLE_TABLE est plutôt efficace. Un des points problématiques étant tout de même la profusion de colonnes NULL qui sont d'autant plus nombreuses que l'arborescence est complexe. Voyons donc d'autres stratégies pour gérer l'héritage.

Note:

Outre de rajouter beaucoup de colonnes inutilisées dans beaucoup d'enregistrements, cette stratégie a aussi pour effet de bord d'empècher de placer une contrainte NOT NULL sur un champ.

Ainsi, même si le champ parent ne peut pas être nul pour un Nain, la base de données ne peut pas imposer cette contrainte puisque la colonne sera nulle pour les instances d'autres classes.

JOINED

La stratégie que nous allons examiner maintenant est peut-être celle que vous auriez choisie si vous aviez mis en oeuvre la persistance de manière artisanale. Surtout si vous avez déjà une expérience des bases de données. L'idée est de stocker les données de chaque classe dans une table, et d'utiliser une jointure pour extraire l'ensemble des données d'un objet. La configuration de cette stratégie se fait à l'aide de l'annotation @Inheritance introduite à la section précédente:

/* ... */
 
@Entity
@Inheritance(strategy=InheritanceType.JOINED)
class Personnage {
    /* ... */
}

L'exécution ne change pas:

sh$ javac fr/chicoree/jpa/heroes/*.java
sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Nain]: Gloin fils de Groin brandit son arme
     Smaug [fr.chicoree.jpa.heroes.Dragon]: Smaug crache le feu
    Thorin [fr.chicoree.jpa.heroes.Nain]: Thorin fils de Thrain brandit son arme

Par contre, dans la base de données les choses ne sont plus les mêmes:

mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Dragon               |
| Nain                 |
| Personnage           |
+----------------------+
3 rows in set (0.00 sec)

Il y a dorénavant bel et bien une table pour chaque classe de l'arborescence. Du coup, plus besoin d'enregistrer explicitement l'information de classe: la présence des données dans une table veut forcément dire que l'on est face à une instance de l'entité correspondante:

mysql> EXPLAIN Personnage;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

mysql> EXPLAIN Dragon;
+-------+--------------+------+-----+---------+-------+
| Field | Type         | Null | Key | Default | Extra |
+-------+--------------+------+-----+---------+-------+
| nom   | varchar(255) | NO   | PRI |         |       |
+-------+--------------+------+-----+---------+-------+
1 row in set (0.00 sec) 

mysql> EXPLAIN Nain;
+--------+--------------+------+-----+---------+-------+
| Field  | Type         | Null | Key | Default | Extra |
+--------+--------------+------+-----+---------+-------+
| parent | varchar(255) | YES  |     | NULL    |       |
| nom    | varchar(255) | NO   | PRI |         |       |
+--------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

Remarque:

Si vous vous interrogez sur la redondance du nom du personnage dans chacune des tables, l'explication en est que c'est la clé primaire. C'est donc par ce champ que se fait la jointure entre les données des différentes tables.

La structure des tables nous montre aussi clairement que pour extraire toutes les données nécessaires pour recréer une instance d'une classe dérivée, Hibernate doit utiliser une jointure. C'est ce que nous allons observer dans les exemples ci-dessous.

Ainsi, imaginons que l'on souhaite charger tous les personnages à l'aide de la requête JPQL suivante:

SELECT p FROM Personnage p -- Charge tous les personnages

Dans la stratégie JOINED cela devient en SQL:

SELECT
    CASE
        WHEN Nain.nom IS NOT NULL THEN "Nain"
        WHEN Dragon.nom IS NOT NULL THEN "Dragon"
        WHEN Personnage.nom IS NOT NULL THEN "Personnage"
    END AS DTYPE,
    Personnage.nom,
    Personnage.comportement,
    Nain.parent
FROM Personnage
LEFT OUTER JOIN Nain ON Nain.nom = Personnage.nom
LEFT OUTER JOIN Dragon ON Dragon.nom = Personnage.nom

Comme vous le remarquez, la requête est nettement plus complexe que dans le cas SINGLE_TABLE. Par contre le résultat est identique:

+------------+--------+--------------+--------+
| DTYPE      | nom    | comportement | parent |
+------------+--------+--------------+--------+
| Personnage | Bilbo  |            1 | NULL   |
| Nain       | Gloin  |            1 | Groin  |
| Dragon     | Smaug  |            0 | NULL   |
| Nain       | Thorin |            1 | Thrain |
+------------+--------+--------------+--------+
4 rows in set (0.00 sec)

Et quand à charger la liste des nains?

SELECT
    Personnage.nom AS nom,
    Personnage.comportement AS comportement,
    Nain.parent AS parent
FROM Personnage
INNER JOIN Nain ON Personnage.nom = Nain.nom
mysql> SELECT
    ->     Personnage.nom AS nom,
    ->     Personnage.comportement AS comportement,
    ->     Nain.parent AS parent
    -> FROM Personnage
    -> INNER JOIN Nain ON Personnage.nom = Nain.nom;
+--------+--------------+--------+
| nom    | comportement | parent |
+--------+--------------+--------+
| Gloin  |            1 | Groin  |
| Thorin |            1 | Thrain |
+--------+--------------+--------+
2 rows in set (0.00 sec)

Comme vous le voyez sur les deux exemples ci-dessus, la stratégie JOINED porte bien son nom, puisque une ou plusieurs jointures sont nécessaires à chaque fois que l'on recharge des objets de la base. C'est à ce prix qu'Hibernate peut éliminer du schéma de la base de données la profusion de colonnes null observée avec la stratégie SINGLE_TABLE.

TABLE_PER_CLASS

/* ... */
 
@Entity
@Inheritance(strategy=InheritanceType.TABLE_PER_CLASS)
class Personnage {
    /* ... */
}

L'exécution ne change toujours pas:

sh$ javac fr/chicoree/jpa/heroes/*.java
sh$ java fr.chicoree.jpa.heroes.DemoPersonnage
     Bilbo [fr.chicoree.jpa.heroes.Personnage]: Bilbo passe à l'attaque!
     Gloin [fr.chicoree.jpa.heroes.Nain]: Gloin fils de Groin brandit son arme
     Smaug [fr.chicoree.jpa.heroes.Dragon]: Smaug crache le feu
    Thorin [fr.chicoree.jpa.heroes.Nain]: Thorin fils de Thrain brandit son arme

Et il y a toujours une table par classe:

mysql> SHOW TABLES;
+----------------------+
| Tables_in_jpa-heroes |
+----------------------+
| Dragon               |
| Nain                 |
| Personnage           |
+----------------------+
3 rows in set (0.00 sec)

Par contre, la structure de ces tables n'est plus la même:

mysql> EXPLAIN Personnage;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> EXPLAIN Dragon;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> EXPLAIN Nain;
+--------------+--------------+------+-----+---------+-------+
| Field        | Type         | Null | Key | Default | Extra |
+--------------+--------------+------+-----+---------+-------+
| nom          | varchar(255) | NO   | PRI |         |       |
| comportement | int(11)      | YES  |     | NULL    |       |
| parent       | varchar(255) | YES  |     | NULL    |       |
+--------------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

Le point-clé à noter est que les champs de la classe de base sont maintenant présents dans les tables des classes dérivées. Du coup, chaque table contient l'ensemble des données des instances de la classe correspondante. Autrement dit encore, toutes les données de tous les nains sont dans la table Nain. Toutes les données de tous les dragons dans la table Dragon. Et dans Personnage on ne trouve que les personnages qui ne sont ni des nains, ni des dragons.

Par conséquent aucune jointure n'est nécessaire pour extraire les données nécessaires à reconstituer les instances d'une seule et même classe:

mysql> SELECT Nain.nom, Nain.comportement, Nain.parent FROM Nain;
+--------+--------------+--------+
| nom    | comportement | parent |
+--------+--------------+--------+
| Gloin  |            1 | Groin  |
| Thorin |            1 | Thrain |
+--------+--------------+--------+
2 rows in set (0.00 sec)

Par contre, charger tous les personnages (c'est à dire les instances de la classe Personnage et de ses classes dérivées) est autrement plus prohibitif. En effet, cela nécessite une union:

SELECT
    "Personnage" AS DTYPE,
    nom,
    comportement,
    NULL AS parent
FROM Personnage
UNION SELECT
    "Nain" AS DTYPE,
    nom,
    comportement,
    parent
FROM Nain
UNION SELECT
    "Dragon" AS DTYPE,
    nom,
    comportement,
    NULL AS parent
FROM Dragon

La stratégie TABLE_PER_CLASS se révèle donc un bon choix si vous êtes amenés à fréquemment charger des instances d'une classe donnée. Par contre, ce choix n'est pas très judicieux si votre application effectue de nombreuses requêtes polymorphiques – où des instances d'une classe et de ses classes dérivées sont chargées par la même requête.

Conclusion

Comme vous le voyez, JPA nous facilite grandement la vie pour assurer la persistance d'objets appartenant à des arborescences de classes. En outre, cette solution est relativement souple puisqu'elle nous permet de choisir parmi différentes stratégies. Néanmoins, il n'y a pas une stratégie meilleure que les autres dans tous les cas. Le choix de l'une ou de l'autre dépendra de l'arborescence de vos classes, de la distribution des champs dans ces classes, et de l'utilisation que vous ferez de vos données.

Par contre, où JPA brille, c'est à cacher cette complexité. Ainsi, passer d'une stratégie à l'autre se résume finalement à changer une annotation dans le code source. Imaginez un peu le travail si vous deviez vous charger de ce travail à la main...

J'espère donc que ce rapide tour d'horizon vous a convaincu que JPA est une solution à même d'assurer de manière relativement transparente la persistance d'objets appartenant à des hiérarchies de classes. Ou en tous cas que cela vous a donné envie d'en savoir plus.