Глава 8. Ассоциативные отображения

Оглавление
  1. 8.1. Введение
  2. 8.2. Однонаправленные ассоциации
    1. 8.2.1. Многие-к-одному
    2. 8.2.2. Один-к-одному
    3. 8.2.3. Один-ко-многим
  3. 8.3. Однонаправленные ассоциации с присоединением таблиц
    1. 8.3.1. Один-ко-многим
    2. 8.3.2. Многие-к-одному
    3. 8.3.3. Один-к-одному
    4. 8.3.4. Многие-ко-многим
  4. 8.4. Двунаправленные ассоциации
    1. 8.4.1. Один-ко-многим / Многие-к-одному
    2. 8.4.2. Один-к-одному
  5. 8.5. Двунаправленные ассоциации с присоединением таблиц
    1. 8.5.1. Один-ко-многим / Многие-к-одному
    2. 8.5.2. Один-к-одному
    3. 8.5.3. Многие-ко-многим
  6. 8.6. Более сложные ассоциативные отображения

8.1. Введение

Ассоциативные отображения часто являются наиболее сложными в плане корректной реализации. В этом разделе мы рассмотрим несколько канонических случаев один за одном, начиная с однонаправленных отображений, а затем двунаправленных. Мы будем использовать Person и Address во всех примерах.

Ассоциации будут классифицированы по множественности и, в зависимости от этого, будут отображены в промежуточной таблицей.

Обнуляемые (nullable) внешние ключи не считаются хорошей практикой в традиционном моделировании данных, поэтому наши примеры не используют обнуляемые (nullable) внешние ключи. Это не является обязательным условием Hibernate, и Отображения будут работать, если вы снимете ограничения по null.

8.2. Однонаправленные ассоциации

8.2.1. Многие-к-одному

Однонаправленная ассоциация «многие-к-одному» является наиболее распространённым видом однонаправленной связи.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address" 
        column="addressId"
        not-null="true"/>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )

8.2.2. Один-к-одному

Однонаправленная ассоциация «один-к-одному» по внешнему ключу почти идентична. Единственное отличие — ограничение unique столбца.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address" 
        column="addressId" 
        unique="true"
        not-null="true"/>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

Однонаправленная ассоциация «один-к-одному» по первичному ключу обычно использует специальный генератор идентификаторов. В этом примере, однако, мы изменили направление ассоциации:

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
</class>
<class name="Address"> <id name="id" column="personId"> <generator class="foreign"> <param name="property">person</param> </generator> </id> <one-to-one name="person" constrained="true"/> </class>
create table Person ( personId bigint not null primary key )
create table Address ( personId bigint not null primary key )

8.2.3. Один-ко-многим

Однонаправленная ассоциация «один-ко-многим» по внешнему ключу является необычным случаем и не рекомендуется.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses">
        <key column="personId" 
            not-null="true"/>
        <one-to-many class="Address"/>
    </set>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key )
create table Address ( addressId bigint not null primary key, personId bigint not null )

Вместо этого вы должны использовать соединение (join) таблиц для такой ассоциации.

8.3. Однонаправленные ассоциации с присоединением таблиц

8.3.1. Один-ко-многим

Однонаправленная ассоциация «один-ко-многим» с присоединением таблицы является предпочтительным. Указание unique=«true» изменяет множественность с  «многие-ко-многим» на «один-ко-многим».

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            unique="true"
            class="Address"/>
    </set>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null primary key )
create table Address ( addressId bigint not null primary key )

8.3.2. Многие-к-одному

Однонаправленная ассоциация «многие-ко-одному» с присоединением таблицы является общей, когда ассоциация необязательна. Например:

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress" 
        optional="true">
        <key column="personId" unique="true"/>
        <many-to-one name="address"
            column="addressId" 
            not-null="true"/>
    </join>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )

8.3.3. Один-к-одному

Однонаправленная ассоциация «один-к-одному» с присоединением таблицы возможна, но чрезвычайно необычна.
<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress" 
        optional="true">
        <key column="personId" 
            unique="true"/>
        <many-to-one name="address"
            column="addressId" 
            not-null="true"
            unique="true"/>
    </join>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

8.3.4. Многие-ко-многим

Наконец, вот пример однонаправленной ассоциации «многие-ко-многим».

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            class="Address"/>
    </set>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) )
create table Address ( addressId bigint not null primary key )

8.4. Двунаправленные ассоциации

8.4.1. Один-ко-многим / Многие-к-одному

Двунаправленная ассоциация «многие-к-одному» является наиболее распространенным видом ассоциации. Следующий пример иллюстрирует стандартное отношение родитель/ребёнок.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address" 
        column="addressId"
        not-null="true"/>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <set name="people" inverse="true"> <key column="addressId"/> <one-to-many class="Person"/> </set> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null )
create table Address ( addressId bigint not null primary key )

Если вы используете List или другую индексированную коллекцию, установите для столбца внешнего ключа значение not null. Hibernate будет управлять ассоциацией со стороны коллекций, чтобы поддерживать индекс каждого элемента, делая другую сторону практически обратной, установив update=«false» и insert=«false»:

<class name="Person">
   <id name="id"/>
   ...
   <many-to-one name="address"
      column="addressId"
      not-null="true"
      insert="false"
      update="false"/>
</class>
<class name="Address"> <id name="id"/> ... <list name="people"> <key column="addressId" not-null="true"/> <list-index column="peopleIdx"/> <one-to-many class="Person"/> </list> </class>

Если низлежащий столбец внешнего ключа NOT NULL, важно, чтобы вы определяли not-null=«true» в элементе <key> отображения коллекции. Не объявляйте только not-null=«true» на возможном вложенном элементе <column>, но и на элементе <key>.

8.4.2. Один-к-одному

Двунаправленная ассоциация «один-к-одному» по внешнему ключу является распространённой:

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <many-to-one name="address" 
        column="addressId" 
        unique="true"
        not-null="true"/>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <one-to-one name="person" property-ref="address"/> </class>
create table Person ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

Двунаправленная ассоциация «один-к-одному» по первичному ключу использует специальный генератор id:

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <one-to-one name="address"/>
</class>
<class name="Address"> <id name="id" column="personId"> <generator class="foreign"> <param name="property">person</param> </generator> </id> <one-to-one name="person" constrained="true"/> </class>
create table Person ( personId bigint not null primary key )
create table Address ( personId bigint not null primary key )

8.5. Двунаправленные ассоциации с присоединением таблиц

8.5.1. Один-ко-многим / Многие-к-одному

Ниже приведён пример двунаправленной ассоциации «один-ко-многим» с присоединением таблицы . Обратное inverse=«true» может идти в любом конце ассоциации, в коллекции или в соединении.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" 
        table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            unique="true"
            class="Address"/>
    </set>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <join table="PersonAddress" inverse="true" optional="true"> <key column="addressId"/> <many-to-one name="person" column="personId" not-null="true"/> </join> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null primary key )
create table Address ( addressId bigint not null primary key )

8.5.2. Один-к-одному

Двунаправленная ассоциация «один-к-одному» с присоединением таблицы возможна, но чрезвычайно необычна.

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <join table="PersonAddress" 
        optional="true">
        <key column="personId" 
            unique="true"/>
        <many-to-one name="address"
            column="addressId" 
            not-null="true"
            unique="true"/>
    </join>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <join table="PersonAddress" optional="true" inverse="true"> <key column="addressId" unique="true"/> <many-to-one name="person" column="personId" not-null="true" unique="true"/> </join> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null primary key, addressId bigint not null unique )
create table Address ( addressId bigint not null primary key )

8.5.3. Многие-ко-многим

Ниже приведён пример двунаправленной ассоциации «многие-ко-многим».

<class name="Person">
    <id name="id" column="personId">
        <generator class="native"/>
    </id>
    <set name="addresses" table="PersonAddress">
        <key column="personId"/>
        <many-to-many column="addressId"
            class="Address"/>
    </set>
</class>
<class name="Address"> <id name="id" column="addressId"> <generator class="native"/> </id> <set name="people" inverse="true" table="PersonAddress"> <key column="addressId"/> <many-to-many column="personId" class="Person"/> </set> </class>
create table Person ( personId bigint not null primary key )
create table PersonAddress ( personId bigint not null, addressId bigint not null, primary key (personId, addressId) )
create table Address ( addressId bigint not null primary key )

8.6. Более сложные ассоциативные отображения

Более сложные объединения (joins) ассоциаций крайне редки. Hibernate обрабатывает более сложные ситуации, используя фрагменты SQL, встроенные в документ отображения. Например, если таблица с историческими данными учётной записи определяет AccountNumber, effectiveEndDate и effectiveStartDatecolumns, она будет отображаться следующим образом:

<properties name="currentAccountKey">
    <property name="accountNumber" type="string" not-null="true"/>
    <property name="currentAccount" type="boolean">
        <formula>case when effectiveEndDate is null then 1 else 0 end</formula>
    </property>
</properties>
<property name="effectiveEndDate" type="date"/>
<property name="effectiveStateDate" type="date" not-null="true"/>

Затем вы можете отобразить ассоциацию на текущий экземпляр, с нулевым (null) effectiveEndDate, используя:

<many-to-one name="currentAccountInfo"
        property-ref="currentAccountKey"
        class="AccountInfo">
    <column name="accountNumber"/>
    <formula>'1'</formula>
</many-to-one>

В более сложном примере представьте, что ассоциация между Employee и Organization поддерживается в таблице Employment, полной исторических данных о занятости. Ассоциация с самым последним работодателем сотрудника, с последним startDate, может быть отображена следующим образом:

<join>
    <key column="employeeId"/>
    <subselect>
        select employeeId, orgId 
        from Employments 
        group by orgId 
        having startDate = max(startDate)
    </subselect>
    <many-to-one name="mostRecentEmployer" 
            class="Organization" 
            column="orgId"/>
</join>

Эта функциональность обеспечивает определенную степень креативности и гибкости, но более практично обращаться с такими случаями с использованием HQL или запроса критериев (criteria query).