В этой статье покажу подводные камни одностороннего отношения OneToMany, и почему его лучше не использовать.
Модель возьмем прежнюю — топик и комментарии к нему:

Только теперь класс Topic будет ссылаться на коллекцию комментариев в одностороннем порядке, а не наоборот.
Модель
Класс Comment:
@Entity
public class Comment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String text;
// getters/setters/constructors
}
Класс Topic:
@Entity
public class Topic {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String title;
@OneToMany(cascade = CascadeType.ALL, orphanRemoval = true)
@JoinColumn(name = "topic_id")
private List<Comment> comments=new ArrayList<>();
//getters/setters/constructors
}
Как видите, коллекция комментариев аннотирована @OneToMany.
При запуске тестов таблицы заполняются данными — они находятся в файле data.sql.
Для демонстрации проблемы попробуем выполнить два теста: добавить топик и удалить комментарий из топика. Сразу скажу, эти операции выполняются, но с потерей производительности, потому что генерируются дополнительные SQL.
Добавление топика
Итак, при добавлении одного топика с тремя комментариями на первый взгляд должно выполниться 4 оператора insert. Но это не так. Попробуем выполнить тест:
@Test
@DisplayName("при добавлении топика для каждого комментария выполняется insert и update")
public void whenAddTopicWithComments_thenInsertsWithUpdates() {
Topic topic = new Topic("title");
topic.getComments().add(new Comment("c1"));
topic.getComments().add(new Comment("c2"));
topic.getComments().add(new Comment("c3"));
topic = topicRepository.save(topic);
Assertions.assertEquals(4, topicRepository.count());
}
В консоли получим:
Hibernate: insert into topic (title, id) values (?, ?) Hibernate: insert into comment (text, id) values (?, ?) Hibernate: insert into comment (text, id) values (?, ?) Hibernate: insert into comment (text, id) values (?, ?) Hibernate: update comment set topic_id=? where id=? Hibernate: update comment set topic_id=? where id=? Hibernate: update comment set topic_id=? where id=?
Hibernate сначала все добавляет, а потом для каждого комментария обновляет столбец topic_id .
Удаление комментария из топика
Аналогичная проблема при удалении комментария. Попробуем удалить один комментарий из топика:
@Test
@DisplayName("при удалении комментария для него выполняется update и delete")
public void whenDeleteComment_thenDeleteWithUpdate() {
Topic topic = topicRepository.getOne(-1l);
topic.getComments().remove(0);
Assertions.assertEquals(2, commentRepository.count());
}
Тут мы удалили комментарий из коллекции, так что топик перестал на него ссылаться. Поскольку orphanRemoval = true, комментарий должен удаляться также из таблицы комментариев. То есть требуется один оператор delete. Но в реальности это не так:
Hibernate: update comment set topic_id=null where topic_id=? and id=? Hibernate: delete from comment where id=?
Сначала у комментария обнуляется поле topic_id, а затем он удаляется.
Чтобы избежать вышеприведенных операторов update, придется поменять отношение с unidirectional на bidirectional, сделать сторону @ManyToOne главной и работать с ней. Так эффективнее.
Замена одностороннего @OneToMany на двустороннее @OneToMany + @ManyToOne
Итак, добавим поле topic в класс Comment:
@Entity
public class GoodComment {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String text;
@ManyToOne(fetch = FetchType.LAZY)
private GoodTopic topic;
//getters/setters/constructors
}
И сделаем коллекцию не основной стороной с помощью mappedBy. Также уберем JoinColumn, он тут не нужен:
@Entity
public class GoodTopic {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private long id;
private String title;
@OneToMany(mappedBy = "topic", cascade = CascadeType.ALL, orphanRemoval = true)
private List<GoodComment> comments=new ArrayList<>();
public void addComment(GoodComment comment) {
comments.add(comment);
comment.setTopic(this);
}
public void removeComment(GoodComment comment) {
comments.remove(comment);
comment.setTopic(null);
}
//getters/setters/constructors
}
Обратите внимание, что добавлены методы addComment() и removeComment(), в них поддерживается согласованность двустороннего отношения.
Теперь при выполнении аналогичных тестов лишних операторов update нет:
@DataJpaTest
public class GoodStructureTest {
@Autowired
private GoodTopicRepository topicRepository;
@Autowired
private GoodCommentRepository commentRepository;
@Test
@DisplayName("при добавлении топика для каждого комментария выполняется один insert")
public void whenAddTopicWithComments_thenInsertsWithUpdates() {
GoodTopic topic = new GoodTopic("title");
topic.addComment(new GoodComment("c1"));
topic.addComment(new GoodComment("c2"));
topic.addComment(new GoodComment("c3"));
topic = topicRepository.save(topic);
Assertions.assertEquals(4, topicRepository.count());
}
@Test
@DisplayName("при удалении комментария для него выполняется один delete")
public void whenDeleteComment_thenDeleteWithUpdate() {
GoodTopic topic = topicRepository.getOne(-11l);
topic.removeComment(topic.getComments().get(0));
Assertions.assertEquals(2, commentRepository.count());
}
}
Итоги
Итак, использование одностороннего @OneToMany чревато лишними SQL операторами. Если коллекция все же необходима, лучше сделать отношение двусторонним (bidirectional): добавить ссылку с противоположного конца отношения и работать со стороной @ManyToOne.
Можно использовать одностороннее отношение @ManyToOne, а для получения элементов коллекции написать отдельный запрос.
Исходный код есть на GitHub.
А что эффективнее? OneToMany + ManyToOne или то о чём вы пишите здесь: «Можно использовать одностороннее отношение @ManyToOne, а для получения элементов коллекции написать отдельный запрос.»
В обоих случаях получить элементы коллекции можно единственным select. Тут лучше смотреть на то, какие объекты. Например, если это многотысячная коллекция сообщений, приходящих юзеру, то нет смысла держать ассоциацию @OneToMany List — слишком много памяти занимает. Эту коллекцию все равно при надобности можно получить с помощью jpql-запроса (причем лучше постраничного), даже если у нас одностороннее отношение @ManyToOne.
В этом смысл фразы.
Добрый день.
Пишу совершенно без придирки, просто хотелось узнать, по какой причине лист comments в топике загружается жадно, в то время как сам топик в листе загружается лениво (в случае двусторонней связи между сущностями)? Если я не ошибаюсь, это может вызвать проблему n+1 select’a при загрузке топика из бд со всеми вытекающими.
Если это просто было неважно в контексте данной темы, я пойму. Спасибо!
Приветствую.
А если есть таблица ролей с ролями ROLE_ADMIN и ROLE_USER, и например пользователи admin (может иметь обе роли одновременно) и пользователь user (имеет только роль ROLE_USER), то какое отношение выставлять между таблицами? При ManyToMany все работает, при OneToMany каждый пользователь может обладать только одной ролью…
не похоже, что между этими таблицами требуется отношение. И потом, в статье про отношение между сущностями, а не таблицами — расставляешь аннотации и генерируются таблицы, вопрос странно поставлен.