Какая проблема возникает в Hibernate при реализации множественных связей между сущностями
Что проверяют
Знаете ли вы про MultipleBagFetchException и декартово произведение, это самый частый смысл вопроса. Следом спрашивают, почему замена List на Set не решает проблему по-настоящему и как загрузить две коллекции без N+1.
Как отвечать
Сценарий: у сущности Post две коллекции @OneToMany, comments и tags, обе объявлены как List. Если загрузить их одним запросом с двумя JOIN FETCH или пометить обе как EAGER, Hibernate бросит MultipleBagFetchException: cannot simultaneously fetch multiple bags.
- bag:
Listбез@OrderColumnдля Hibernate это неупорядоченная коллекция с дублями. Два таких соединения в одном SQL дают декартово произведение, и Hibernate не может отличить настоящие элементы от повторов; - обход через
Set: исключение пропадёт, декартово произведение останется. 50 комментариев и 20 тегов дают 1000 строк на каждый пост; - правильное решение: грузить коллекции отдельными запросами в одном контексте персистентности. Первый запрос тянет посты с
comments, второй те же посты сtags, и Hibernate сам соберёт граф. Альтернативы:@BatchSizeилиhibernate.default_batch_fetch_size,@Fetch(FetchMode.SUBSELECT); - N+1: обратная сторона, если ничего не подгружать заранее. Лечится теми же батчами или
@EntityGraph.
У @ManyToMany своя ловушка: если связь объявлена как List, удаление одного элемента заставляет Hibernate стереть все строки связующей таблицы для этой сущности и вставить оставшиеся заново. С Set удаляется одна строка. Для Set нужны корректные equals и hashCode: по бизнес-ключу или по id с постоянным hashCode, иначе сущность теряется в множестве после сохранения.
Где спотыкаются
Меняют List на Set и считают задачу закрытой. На реальном объёме запрос возвращает сотни тысяч строк, и это всплывает уже в продакшене.
Ставят CascadeType.ALL на @ManyToMany. Каскад REMOVE удалит связанные сущности, которыми пользуются и другие записи: удалили пост, пропали теги у всех постов.
Лечат всё через FetchType.EAGER. Жадная загрузка срабатывает на каждом запросе к сущности, а с несколькими коллекциями снова приводит к той же ошибке.