«1. Обзор

В этом руководстве мы рассмотрим различные способы создания взаимно однозначных сопоставлений в JPA.

Нам потребуется базовое понимание фреймворка Hibernate, поэтому ознакомьтесь с нашим руководством по Hibernate 5 с Spring для дополнительной информации.

2. Описание

Предположим, мы создаем систему управления пользователями, и наш начальник просит нас сохранить почтовый адрес для каждого пользователя. У пользователя будет один почтовый адрес, а к почтовому адресу будет привязан только один пользователь.

Это пример отношения один к одному, в данном случае между пользователем и адресом.

Давайте посмотрим, как мы можем реализовать это в следующих разделах.

3. Использование внешнего ключа

3.1. Моделирование с помощью внешнего ключа

Давайте посмотрим на следующую ER-диаграмму, которая представляет взаимно-однозначное сопоставление на основе внешнего ключа:

An ER Diagram mapping Users to Addresses via an address_id foreign key

В этом примере столбец address_id в users является внешним ключом для адреса .

3.2. Реализация с помощью внешнего ключа в JPA

Во-первых, давайте создадим класс User и добавим к нему соответствующую аннотацию:

@Entity
@Table(name = "users")
public class User {
    
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;
    //... 

    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "address_id", referencedColumnName = "id")
    private Address address;

    // ... getters and setters
}

Обратите внимание, что мы размещаем аннотацию @OneToOne в связанном поле сущности Address.

Кроме того, нам нужно разместить аннотацию @JoinColumn, чтобы настроить имя столбца в таблице пользователей, которое сопоставляется с первичным ключом в таблице адресов. Если мы не укажем имя, Hibernate будет следовать некоторым правилам, чтобы выбрать имя по умолчанию.

Наконец, обратите внимание, что в следующем объекте мы не будем использовать аннотацию @JoinColumn. Это потому, что нам это нужно только на стороне владения внешним ключом. Проще говоря, тот, кто владеет столбцом внешнего ключа, получает аннотацию @JoinColumn.

Сущность Address получается немного проще:

@Entity
@Table(name = "address")
public class Address {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;
    //...

    @OneToOne(mappedBy = "address")
    private User user;

    //... getters and setters
}

Нам также нужно разместить здесь аннотацию @OneToOne. Это потому, что это двунаправленная связь. Адресная сторона отношения называется невладеющей стороной.

4. Использование общего первичного ключа

4.1. Моделирование с общим первичным ключом

В этой стратегии вместо создания нового столбца address_id мы пометим столбец первичного ключа (user_id) таблицы адресов как внешний ключ для таблицы пользователей:

An ER diagram with Users Tied to Addresses where they share the same primary key values

Мы’ Мы оптимизировали пространство для хранения, используя тот факт, что эти объекты имеют отношение один к одному между собой.

4.2. Реализация с общим первичным ключом в JPA

Обратите внимание, что наши определения изменились лишь незначительно:

@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    //...

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL)
    @PrimaryKeyJoinColumn
    private Address address;

    //... getters and setters
}
@Entity
@Table(name = "address")
public class Address {

    @Id
    @Column(name = "user_id")
    private Long id;

    //...

    @OneToOne
    @MapsId
    @JoinColumn(name = "user_id")
    private User user;
   
    //... getters and setters
}

Атрибут mappedBy теперь перемещен в класс User, так как внешний ключ теперь присутствует в таблице адресов. Мы также добавили аннотацию @PrimaryKeyJoinColumn, указывающую, что первичный ключ сущности User используется в качестве значения внешнего ключа для связанной сущности Address.

Нам еще нужно определить поле @Id в классе Address. Но обратите внимание, что это ссылается на столбец user_id и больше не использует аннотацию @GeneratedValue. Кроме того, в поле, которое ссылается на пользователя, мы добавили аннотацию @MapsId, указывающую, что значения первичного ключа будут скопированы из сущности пользователя.

5. Использование таблицы соединений

Отображения \»один к одному\» могут быть двух типов: необязательные и обязательные. До сих пор мы видели только обязательные отношения.

Теперь давайте представим, что наши сотрудники связаны с рабочей станцией. Это один к одному, но иногда у сотрудника может не быть рабочей станции и наоборот.

5.1. Моделирование с помощью таблицы соединений

Стратегии, которые мы обсуждали до сих пор, вынуждают нас помещать нулевые значения в столбец для обработки необязательных отношений.

An ER diagram relating Employees to Workstations via a Join Table

Обычно мы думаем об отношениях «многие ко многим», когда рассматриваем таблицу соединений, но использование таблицы соединений в этом случае может помочь нам устранить эти нулевые значения:

Теперь, когда у нас есть отношение, мы сделаем запись в таблице emp_workstation и полностью избежим нулей.

5.2. Реализация с помощью таблицы соединений в JPA

@Entity
@Table(name = "employee")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    //...

    @OneToOne(cascade = CascadeType.ALL)
    @JoinTable(name = "emp_workstation", 
      joinColumns = 
        { @JoinColumn(name = "employee_id", referencedColumnName = "id") },
      inverseJoinColumns = 
        { @JoinColumn(name = "workstation_id", referencedColumnName = "id") })
    private WorkStation workStation;

    //... getters and setters
}
@Entity
@Table(name = "workstation")
public class WorkStation {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    //...

    @OneToOne(mappedBy = "workStation")
    private Employee employee;

    //... getters and setters
}

В нашем первом примере использовалась @JoinColumn. На этот раз мы будем использовать @JoinTable:

@JoinTable инструктирует Hibernate использовать стратегию таблицы соединений, сохраняя связь.

«Кроме того, Employee является владельцем этого отношения, поскольку мы решили использовать для него аннотацию таблицы соединений.

6. Заключение