Отношение OneToOne в Hibernate и Spring

В этой статье рассмотрим  отношение @OneToOne.
Допустим, одному пользователю User соответствует одна сущность UserDetails (информация о User).

Типичный нелучший способ

Обычно отношение @OneToOne делают так:

@Entity
public class UserDetails {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String phone;

    @OneToOne(fetch = FetchType.LAZY)
    private User user;

}

Что создает в таблице USER_DETAILS внешний ключ USER_ID, указывающий на ID в таблице USER:

При этом класс User тоже может ссылаться на UserDetails (или не ссылаться, на схему это не влияет).

Тут User ссылается:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

    @OneToOne(mappedBy = "user", cascade = CascadeType.ALL, optional = false, fetch = FetchType.LAZY)
    private UserDetails userDetails;

}

Добавим данные:

insert into users (id, name) values (1,'Ivan');
insert into users (id, name) values (2, 'John');
insert into users (id, name) values (3, 'Petr');

insert into user_details (id, phone, user_id) values (4, '154623',  1);
insert into user_details (id,  phone, user_id) values (5, '435',  2);
insert into user_details (id,  phone, user_id) values (6, '3454',  3);

Выглядит результат так:

Схема ,без @MapsId
Схема без @MapsId

Недостатки

  • лишний столбец
  • если User тоже в свою очередь ссылается на UserDetails, то его настройка fetch = FetchType.LAZY не работает. То есть при поиске пользователя генерируется не один, а два SQL оператора:

а именно, такой поиск

@DataJpaTest
class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    @DisplayName("ищет user EAGER")
    public void whenFindUser_ThenEager() {
        Optional<User> optionalUser = userRepository.findById(1l);
        Assertions.assertTrue(optionalUser.isPresent());
    }

}

генерирует два SQL оператора:

select user0_.id as id1_1_0_, user0_.name as name2_1_0_ 
from users user0_ where user0_.id=?

select userdetail0_.id as id1_0_0_, userdetail0_.phone as phone2_0_0_, userdetail0_.user_id as user_id3_0_0_ 
from user_details userdetail0_ 
where userdetail0_.user_id=?

Это неоптимально.

Лучший способ

Во-первых, можно убрать из схемы лишний столбец. Если у каждого UserDetails свой ровно один User, то зачем в таблице USER_DETAILS нужен автогенерируемый первичный ключ? Достаточно одного USER_ID – пусть он будет и первичный, и внешний:

Схема с mapsid
Схема с mapsid

 

Схема с @MapsId
Схема с @MapsId

Чтобы создать такую схему, надо в UserDetails аннотировать поле user аннотацией @MapsId:

@Entity
public class UserDetails {
    @Id
    private long id;

    private String phone;

    @OneToOne(fetch = FetchType.LAZY)
    @MapsId
    private User user;

}

Также у поля id выше в убрана аннотация @GeneratedValue. Теперь id не генерируется автоматически, а заполняется идентификатором User.

Помимо более чистой структуры БД, для поля userDetails сущности User начинает работать FetchType.LAZY, то есть при поиске User по id уже выполняется один select, а не два.

Но все же из User лучше обратную ссылку убрать:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    private long id;

    private String name;

}

Заполним новую схему данными:

insert into users (id, name) values (1,'Ivan');
insert into users (id, name) values (2, 'John');
insert into users (id, name) values (3, 'Petr');

insert into user_details (phone, user_id) values ('154623',  1);
insert into user_details (phone, user_id) values ('435',  2);
insert into user_details (phone, user_id) values ('3454',  3);

Выполним тот же тест, и получим один оператор select:

select user0_.id as id1_1_0_, user0_.name as name2_1_0_ 
   from users user0_ where user0_.id=?

Зная идентификатор User, всегда можно извлечь UserDetails по такому же идентификатору. И уж тогда получить второй select.

Переименование внешнего/первичного ключа

Чтобы сменить название внешнего ключа (который по сути является первичным) в таблице user_details с user_id на id, нужно использовать аннотацию @JoinColumn:

@MapsId
@JoinColumn(name = "id")
private User user;

Получим такую схему:

Переименование столбца с помощью @JoinColumn
Переименование столбца с помощью @JoinColumn

Итоги

Таким образом, второй вариант с @MapsId предпочтительней: лучше совместить внешний и первичный ключ, а также делать одностороннее отношение (без обратного поля с mappedBy). Это оптимально для производительности.

Исходный код на GitHub.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *