Chapitre 8. Mapping de l'héritage de classe

8.1. Les trois stratégies

Hibernate supporte les trois stratégies d'héritage de base.

  • une table par hiérarchie de classe (table per class hierarchy)

  • une table par classe fille (table per subclass)

  • une table par classe concrête (table per concrete class, avec limitations)

Il est même possible d'utiliser différentes stratégies de mapping pour différentes branches d'une même hiérarchie d'héritage, mais les mêmes limitations, que celle rencontrées dans la stratégie une table par classe concrète, s'appliquent. Hibernate ne supporte pas le mélange des mappings <subclass> et <joined-subclass> dans un même élément <class>.

Supposons que nous ayons une interface Payment, implémentée par CreditCardPayment, CashPayment, ChequePayment. La stratégie une table par hiérarchie serait :

<class name="Payment" table="PAYMENT">
    <id name="id" type="long" column="PAYMENT_ID">
        <generator class="native"/>
    </id>
    <discriminator column="PAYMENT_TYPE" type="string"/>
    <property name="amount" column="AMOUNT"/>
    ...
    <subclass name="CreditCardPayment" discriminator-value="CREDIT">
        ...
    </subclass>
    <subclass name="CashPayment" discriminator-value="CASH">
        ...
    </subclass>
    <subclass name="ChequePayment" discriminator-value="CHEQUE">
        ...
    </subclass>
</class>

Une seule table est requise. Une grande limitation de cette stratégie est que les colonnes déclarées par les classes filles ne peuvent avoir de contrainte NOT NULL.

La stratégie une table par classe fille serait :

<class name="Payment" table="PAYMENT">
    <id name="id" type="long" column="PAYMENT_ID">
        <generator class="native"/>
    </id>
    <property name="amount" column="AMOUNT"/>
    ...
    <joined-subclass name="CreditCardPayment" table="CREDIT_PAYMENT">
        <key column="PAYMENT_ID"/>
        ...
    </joined-subclass>
    <joined-subclass name="CashPayment" table="CASH_PAYMENT">
        <key column="PAYMENT_ID"/>
        ...
    </joined-subclass>
    <joined-subclass name="ChequePayment" table="CHEQUE_PAYMENT">
        <key column="PAYMENT_ID"/>
        ...
    </joined-subclass>
</class>

Quatre tables sont requises. Les trois tables des classes filles ont une clé primaire associée à la table classe mère (le modèle relationnel est une association un-vers-un).

Notez que l'implémentation Hibernate de la stratégie un table par classe fille ne nécessite pas de colonne discriminante dans la table classe mère. D'autres implémentations de mappers Objet/Relationnel utilisent une autre implémentation de la stratégie une table par classe fille qui nécessite une colonne de type discriminant dans la table de la classe mère. L'approche prise par Hibernate est plus difficile à implémenter mais plus correcte d'une point de vue relationnel.

Pour chacune de ces deux stratégies de mapping, une association polymorphique vers Payment est mappée en utilisant <many-to-one>.

<many-to-one name="payment"
    column="PAYMENT"
    class="Payment"/>

La stratégie une table par classe concrète est très différente.

<class name="CreditCardPayment" table="CREDIT_PAYMENT">
    <id name="id" type="long" column="CREDIT_PAYMENT_ID">
        <generator class="native"/>
    </id>
    <property name="amount" column="CREDIT_AMOUNT"/>
    ...
</class>

<class name="CashPayment" table="CASH_PAYMENT">
    <id name="id" type="long" column="CASH_PAYMENT_ID">
        <generator class="native"/>
    </id>
    <property name="amount" column="CASH_AMOUNT"/>
    ...
</class>

<class name="ChequePayment" table="CHEQUE_PAYMENT">
    <id name="id" type="long" column="CHEQUE_PAYMENT_ID">
        <generator class="native"/>
    </id>
    <property name="amount" column="CHEQUE_AMOUNT"/>
    ...
</class>

Trois tables sont requises. Notez que l'interface Payment n'est jamais explicitement nommée. A la place, nous utilisons le polymorphisme implicite d'Hibernate. Notez aussi que les propriétés de Payment sont mappées dans chacune des classes filles.

Dans ce cas, une association polymorphique vers Payment est mappée en utilisant <any>.

<any name="payment"
        meta-type="class"
        id-type="long">
    <column name="PAYMENT_CLASS"/>
    <column name="PAYMENT_ID"/>
</any>

Il serait plus judicieux de définir un UserType comme meta-type, pour gérer le mapping entre une chaîne de caractère discriminante et les classes filles de Payment.

<any name="payment"
        meta-type="PaymentMetaType"
        id-type="long">
    <column name="PAYMENT_TYPE"/> <!-- CREDIT, CASH or CHEQUE -->
    <column name="PAYMENT_ID"/>
</any>

Il y a une autre chose à savoir sur ce mapping. Dans la mesure où chaque classe fille est mappée dans leur propre élément <class> (et puisque Payment n'est qu'une interface), chacune des classes filles peut facilement faire partie d'une autre stratégie d'héritage que cela soit une table par hiérarchie ou une table par classe fille ! (et vous pouvez toujours utiliser des requêtes polymorphiques vers l'interface) Payment).

<class name="CreditCardPayment" table="CREDIT_PAYMENT">
    <id name="id" type="long" column="CREDIT_PAYMENT_ID">
        <generator class="native"/>
    </id>
    <discriminator column="CREDIT_CARD" type="string"/>
    <property name="amount" column="CREDIT_AMOUNT"/>
    ...
    <subclass name="MasterCardPayment" discriminator-value="MDC"/>
    <subclass name="VisaPayment" discriminator-value="VISA"/>
</class>

<class name="NonelectronicTransaction" table="NONELECTRONIC_TXN">
    <id name="id" type="long" column="TXN_ID">
        <generator class="native"/>
    </id>
    ...
    <joined-subclass name="CashPayment" table="CASH_PAYMENT">
        <key column="PAYMENT_ID"/>
        <property name="amount" column="CASH_AMOUNT"/>
        ...
    </joined-subclass>
    <joined-subclass name="ChequePayment" table="CHEQUE_PAYMENT">
        <key column="PAYMENT_ID"/>
        <property name="amount" column="CHEQUE_AMOUNT"/>
        ...
    </joined-subclass>
</class>

Encore une fois, nous ne mentionnons pas explicitement Payment. Si nous exécutons une requête sur l'interface Payment - par exemple, from Payment - Hibernate retournera automatiquement les instances de CreditCardPayment (et ses classes filles puisqu'elles implémentent aussi Payment), CashPayment et ChequePayment mais pas les instances de NonelectronicTransaction.

8.2. Limitations

Hibernate suppose qu'une association mappe exactement une colonne clé étrangère. Plusieurs associations par clé étrangère sont tolérées (vous pouvez avoir besoin de spécifier inverse="true" ou insert="false" update="false"), mais il n'est pas possible de mapper chaque association vers plusieurs clés étrangères. Ceci signifie que :

  • quand une association est modifiée, c'est toujours la même clé étrangère qui est mise à jour

  • quand une association est chargée de manière tardive, une seule requête à la base de données est utilisée

  • quand une association est chargée immédiatement, elle peut l'être en utilisant une simple jointure ouverte

Ceci implique que les associations polymorphiques un-vers-plusieurs vers des classes mappées en utilisant la stratégie une table par classe concrète ne sont pas supportées (charger ces associations nécessiterait de multiples requêtes ou jointures ouverte).

Le tableau montre les limitations des mappings une table par classe concrète, et du polymorphique implicite, avec Hibernate.

Tableau 8.1. Caractéristiques des stratégies d'héritages

Stratégie d'héritagePlusieurs-vers-un polymorphiquesUn-vers-un polymorphiquesUn-vers-plusieurs polymorphiquesPlusieurs-vers-plusieurs polymorphiquesload()/get() polymorphiquesRequêtes polymorphiquesJointures polymorphiquesJointures ouvertes polymorphiques
une table par hiérarchie de classe<many-to-one><one-to-one><one-to-many><many-to-many>s.get(Payment.class, id)from Payment pfrom Order o join o.payment psupportées
une table par classe fille<many-to-one><one-to-one><one-to-many><many-to-many>s.get(Payment.class, id)from Payment pfrom Order o join o.payment psupportées
une table par classe concrète (polymorphisme implicite)<any>non supportésnon supportés<many-to-any>utiliser un requêtefrom Payment pnon supportéesnon supportées