Глава 8. Маппинг наследников

8.1. Три стратегии

Hibernate поддерживает три базовые стратегии маппинга наследников.

  • table per class hierarchy

  • table per subclass

  • table per concrete class (с некоторыми ограничениями)

Возмножно использование разных стратегий маппинга для различных веток одной и той же иерархии наследников, но есть некоторые ограничения для стратегии маппинга table-per-concrete class. Hibernate не поддерживает одновременного использования <subclass> маппинга и <joined-subclass> маппинга в одном элементе <class>

Допустим у нас есть интерфейс Payment и классы CreditCardPayment, CashPayment, ChequePayment, которые его реализуют. Маппинг table-per-hierarchy может выглядеть так:

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

Используется только одна таблица для всех классов. В этой стратегии маппинга есть одно сильное ограничение: столбцы которые определены в подкассах не могут быть NOT NULL.

Маппинг table-per-subclass может выглядеть примерно так:

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

В данном случае требуются четыре таблицы. Три таблицы подклассов ассоциируются с таблицей суперкласса по первичному ключу (в реляционной модели это в действительности отношение один к одному).

Заметьте, что реализация table-per-subclass в Hibernate не требует столбцов-дискриминаторов. Другие продукты для объекто-реляционного отображения используют модель реализации стратегии table-per-subclass, требующую определения столбца-дискриминатора в таблице суперкласса. Подход используемый в Hibernate намного более труден в реализации, но, вероятно, более корректен с точки зрения проектирования реляционной модели.

Для любой из этих двух стратегий маппинга, полиморфические ассоциации к Payment маппятся используя <many-to-one>.

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

Стратегия маппинга table-per-concrete-class очень сильно отличается от остальных.

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

Требуются три таблицы. Заметьте, что нигде явно не упоминается связь с интерфейсом Payment. Вместо этого используется Hibernate'овский неявный полиморфизм (implicit polymorphism). Так же заметьте, что свойства интерфейса Payment определяются для каждого из подклассов.

В данном случае случае полиморфическая ассоциация к Payment маппится с использованием <any>.

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

Будет лучше, если в качестве meta-type мы укажем пользовательский тип, для определения маппинга подклассов Payment.

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

Есть еще одна дополтинельная вещь, которую нужно учитывать при данном маппинге. Так как подклассы определены в элементах <class> (и Payment интерфейс к ним), каждый из подклассов может в свою очередь использовать стратегии маппинга table-per-class или table-per-subclass! (При этом остается возможность использования полиморфических запросов к интерфейсу 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>

Повторим, что мы не ссылается на Payment явно. При исполнении запроса по интерфейсу Payment, например from Payment, Hibernate автоматически возвращает экземпляры CreditCardPayment (и его подклассы, они так же реализуют интерфейс Payment), CashPayment и ChequePayment но не NonelectronicTransaction. Once again, we don't mention Payment explicitly. If we execute a query against the Payment interface - for example, from Payment - Hibernate automatically returns instances of CreditCardPayment (and its subclasses, since they also implement Payment), CashPayment and ChequePayment but not instances of NonelectronicTransaction.

8.2. Ограничения

Hiberante предполагает, что ассоциация отображается на единственный столбец foreign key. Множественные ассоциации для по одному и тому же foreign key допускаются (возможно потребуется указать inverse="true" или insert="false" update="false"), но не существует возможности отображения одной ассоциации на множество foreign keys. Это означает вот что:

  • когда ассоциация модифицируется, всегда обновляется один и тот же foreign key.

  • когда происходит ленивая выборка по ассоциациям используется один и тот же запрос.

  • когда происходит "стремительная" (eagerly) выборка по ассоциации, используется один и тот же outer join.

Полиморфическая ассоциация один ко многим замапленная с использованием стратегии table-per-concrete-class не поддерживается. (Выборка по таким ассоциациям обычно требует нескольких запросов или множественных join'ов.)

А нижеследующей таблице перечисленны ограничения маппинга table-per-concrete-class и неявного полиморфизма в Hibernate.

Таблица 8.1. Возможности маппинга наследников

Стратегия наследования Polymorphic many-to-one Polymorphic one-to-one Polymorphic one-to-many Polymorphic many-to-many Polymorphic load()/get() Polymorphic queries Polymorphic joins Outer join fetching
table-per-class-hierarchy <many-to-one> <one-to-one> <one-to-many> <many-to-many> s.get(Payment.class, id) from Payment p from Order o join o.payment p поддерживается
table-per-subclass <many-to-one> <one-to-one> <one-to-many> <many-to-many> s.get(Payment.class, id) from Payment p from Order o join o.payment p поддерживается
table-per-concrete-class (неявный полиморфизм) <any> не поддерживается не поддерживается <many-to-any> use a query from Payment p не поддерживается не поддерживается