В этой статье рассмотрим отношение @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);
Выглядит результат так:
Недостатки
- лишний столбец
- если 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 — пусть он будет и первичный, и внешний:
Чтобы создать такую схему, надо в 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;
Получим такую схему:
Итоги
Таким образом, второй вариант с @MapsId предпочтительней: лучше совместить внешний и первичный ключ, а также делать одностороннее отношение (без обратного поля с mappedBy). Это оптимально для производительности.
Исходный код на GitHub.
Есть также вариант сопоставить 1:1 с помощью @SecondaryTable.
Так этот проект использует Hibernate или это чистое JPA?
Просто я в файле pom.xml не вижу ни одной подключенной библиотеки Hibernate. Типа зависимости hibernate-core.
Так же нет hibernate.cfg.xml файла, где прописаны настройки Hibernate.
Или они не обязательны?
Можете немного просвятить в этом вопросе.
Это Spring Boot, тут есть зависимость spring-boot-starter-data-jpa, и в список ее зависимостей как раз и входит hibernate. Файл hibernate.cfg.xml тут не нужен, настройки переходят в application.yml (специфика Spring Boot).
При сохранении нового объекта UserDetails через JpaRepository методом save() выполняется два insert: insert into users, затем insert into user_details. Как избавиться от первого insert?
Т.к. объект User уже до этого был сохранен в БД возникает ошибка уникальности «duplicate key value violates unique constraint»
Все правильно, что выполняется два insert, отношение же OneToOne, так и должно быть.
Проблема в другом — в data.sql уже добавлены три пользователя с id=1,2,3 (их мы выбираем в примере). А в классе User стоит @GeneratedValue(strategy = GenerationType.SEQUENCE) без уточнений, это означает, что создается последовательность hibernate_sequence с начальным значением 1, и с помощью нее генерируются новые id. Поэтому при первом же добавлении генерируется id=1 и возникает ошибка. Исправить это просто. Либо уточнить параметры генератора, сделать начальное значение как минимум с 4:
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = «users_id_seq-generator»)
@SequenceGenerator(name = «users_id_seq-generator», sequenceName= «users_id_seq»,
initialValue = 4, allocationSize = 10)
private long id;
Либо делать @GeneratedValue(strategy = GenerationType.IDENTITY) — так id будет генерироваться без использования sequence. Это будет просто автоинкрементный столбец, где новое значение генерируется в зависимости от предыдущего. Такой вариант и отправлен сейчас в репозиторий вместе с примером сохранения user, спасибо за замечание.
Если по логике объект User может существовать и без UserDetails, то каким образом нужно сохранить новый объект UserDetails чтобы не возникало ошибки(из-за первого insert)?
Так User без UserDetails вроде можно сохранить:
User user=new User();
user.setName(«Jane»);
userRepository.save(user);
А вот UserDetails без User сохранить нельзя, используя @MapsId, потому что с ним такая структура в базе генерируется: первичный ключ в UserDetails (он же является внешним) берется из первичного ключа User. Нет User — нет UserDetails.
Если все же надо сохранять UserDetails без User, то @MapsId не надо использовать.
Это все понятно, я все пытаюсь объяснить проблему когда User уже сохранен в БД и его больше не нужно сохранять.
User user = userRepo.getById(4L);
UserDetails ud = new UserDetails();
ud.setUser(user);
userDetailsRepo.save(ud) // тут ошибка
выполняя первый insert он пытается сохранить нового User, но он уже есть в БД с id 4. Цель — сохраняя UserDetails сохранить только его и не трогать таблицу с User. Есть идеи?
У меня такой тест срабатывает (при условии что userdetails c id=3 правда нет, то есть если из data.sql убрать строку insert into user_details (phone, id) values (‘3454’, 3);):
@Test
@Commit
public void shouldSaveUserDetailsWhenUserExists() {
User user = userRepository.getOne(3l);
UserDetails ud = new UserDetails();
ud.setPhone(«123»);
ud.setUser(user);
userDetailsRepository.save(ud);
}
Если мы убираем аннотацию @OneToOne на сущности User, то пропадает возможность воспользоваться cascade, нужного, например, для удаления удаления User. Значит, нужно в методе удаления юзера сначала проверять, существуют ли для него userdetails, затем удалять их и только после этого удалять юзера. В итоге мы получаем, достаточно длинный sql..
Или же есть другие варианты?