В этой статье покажу подводные камни одностороннего отношения 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 каждый пользователь может обладать только одной ролью…
не похоже, что между этими таблицами требуется отношение. И потом, в статье про отношение между сущностями, а не таблицами — расставляешь аннотации и генерируются таблицы, вопрос странно поставлен.