Автор оригинала: Vlad Mihalcea.
Вступление
Если вы используете Hibernate в течение некоторого времени, есть большая вероятность, что вы столкнулись с проблемой MultipleBagFetchException
:
организация.спящий режим.загрузчик. Исключение MultipleBagFetchException: не удается одновременно извлечь несколько пакетов
В этой статье мы рассмотрим причину, по которой Hibernate выдает исключение MultipleBagFetchException
, а также лучший способ решения этой проблемы.
Модель предметной области
Давайте рассмотрим, что наше приложение определяет три сущности: Сообщение
, Комментарий к сообщению
, и Тег
, которые связаны, как показано на следующей диаграмме:
Что нас больше всего интересует в этой статье, так это то, что Сообщение
сущность определяет двунаправленную @OneToMany связь с Комментарием к сообщению
дочерней сущностью, а также однонаправленную @ManyToMany
ассоциация с Тегом
сущностью.
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private Listcomments = new ArrayList<>(); @ManyToMany( cascade = { CascadeType.PERSIST, CascadeType.MERGE } ) @JoinTable( name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private List tags = new ArrayList<>();
Причина, по которой @ManyToMany
ассоциация каскадирует только СОХРАНЯЕТСЯ
и ОБЪЕДИНИТЬ
переходы в состояние сущности и не УДАЛИТЬ
во-первых, потому, что другая сторона не является дочерней сущностью.
Поскольку жизненный цикл тега
сущности не привязан к Разместить
сущность, каскадировать УДАЛИТЬ
или включить механизм orphanRemoval
было бы ошибкой. Для получения более подробной информации об этой теме ознакомьтесь с этой статьей .
Спящий режим вызывает исключение MultipleBagFetchException
Теперь, если мы хотим получить объекты Post
со значениями идентификаторов от 1 до 50, а также все связанные с ними Сообщения
и Тег
сущности, мы бы написали запрос, подобный следующему:
Listposts = entityManager.createQuery(""" select p from Post p left join fetch p.comments left join fetch p.tags where p.id between :minId and :maxId """, Post.class) .setParameter("minId", 1L) .setParameter("maxId", 50L) .getResultList();
Однако при выполнении приведенного выше запроса сущности Hibernate выдает исключение MultipleBagFetchException
при компиляции запроса JPQL:
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags [ com.vladmihalcea.book.hpjp.hibernate.fetching.Post.comments, com.vladmihalcea.book.hpjp.hibernate.fetching.Post.tags ]
Таким образом, Hibernate не выполняет SQL-запрос. Причина, по которой MultipleBagFetchException
вызывается Hibernate, заключается в том, что могут возникать дубликаты, и неупорядоченный Список
, который в терминологии Hibernate называется bag , не должен удалять дубликаты.
Как НЕ “исправить” исключение Hibernate MultipleBagFetchException
Если вы загуглите исключение MultipleBagFetchException
, вы увидите много неправильных ответов, таких как этот в StackOverflow , который, как ни удивительно, имеет более 280 голосует.
Так просто, но в то же время так неправильно!
Использование набора вместо списка
Итак, давайте изменим тип коллекции ассоциаций с List
на Набор
:
@OneToMany( mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true ) private Setcomments = new HashSet<>(); @ManyToMany( cascade = { CascadeType.PERSIST, CascadeType.MERGE } ) @JoinTable( name = "post_tag", joinColumns = @JoinColumn(name = "post_id"), inverseJoinColumns = @JoinColumn(name = "tag_id") ) private Set tags = new HashSet<>();
И, теперь, при повторном запуске предыдущего запроса сущности, который извлек некоторые Сообщения
сущности вместе с их комментариями
и теги
ассоциации, мы видим, что не возникает MultipleBagFetchException
.
Однако это SQL-запрос, который Hibernate выполняет для вышеупомянутого JPQL-запроса:
SELECT p.id AS id1_0_0_, pc.id AS id1_1_1_, t.id AS id1_3_2_, p.title AS title2_0_0_, pc.post_id AS post_id3_1_1_, pc.review AS review2_1_1_, t.name AS name2_3_2_, pt.post_id AS post_id1_2_1__, pt.tag_id AS tag_id2_2_1__ FROM post p LEFT OUTER JOIN post_comment pc ON p.id = pc.post_id LEFT OUTER JOIN post_tag pt ON p.id = pt.post_id LEFT OUTER JOIN tag t ON pt.tag_id = t.id WHERE p.id BETWEEN 1 AND 50
Итак, что не так с этим SQL-запросом?
Сообщение
и post_comment
связаны через столбец post_id
Внешний ключ, поэтому объединение создает результирующий набор, содержащий все строки post
таблицы со значениями первичного ключа от 1 до 50 вместе с их связанными строками post_comment
таблицы.
Сообщение
и тег
таблицы также связаны с помощью post_id
и tag_id
столбцы post_tag
внешнего ключа, поэтому эти два объединения создают результирующий набор, содержащий все строки post
таблицы со значениями первичного ключа от 1 до 50, а также связанные с ними строки tag
таблицы.
Теперь для объединения двух наборов результатов база данных может использовать только декартово произведение, поэтому конечный набор результатов содержит 50 записей
строк, умноженных на связанные пост_коммент
и пометить
строки таблицы.
Итак, если у нас есть 50 записей
строк, связанных с 20 пост_коммент
и 10 метка
строки, конечный набор результатов будет содержать 10_000 записей (например, 50 x 20 x 10), как показано в следующем тестовом примере:
Listposts = entityManager.createQuery(""" select p from Post p left join fetch p.comments left join fetch p.tags where p.id between :minId and :maxId """, Post.class) .setParameter("minId", 1L) .setParameter("maxId", 50L) .getResultList(); assertEquals( POST_COUNT * POST_COMMENT_COUNT * TAG_COUNT, posts.size() );
Это так ужасно с точки зрения производительности!
Как исправить исключение Hibernate MultipleBagFetchException
Чтобы избежать декартова произведения, вы можете извлекать не более одной ассоциации за раз. Таким образом, вместо выполнения одного JPQL-запроса, который извлекает две ассоциации, мы можем вместо этого выполнить два JPQL-запроса:
Listposts = entityManager.createQuery(""" select distinct p from Post p left join fetch p.comments where p.id between :minId and :maxId""", Post.class) .setParameter("minId", 1L) .setParameter("maxId", 50L) .setHint(QueryHints.PASS_DISTINCT_THROUGH, false) .getResultList(); posts = entityManager.createQuery(""" select distinct p from Post p left join fetch p.tags t where p in :posts""", Post.class) .setParameter("posts", posts) .setHint(QueryHints.PASS_DISTINCT_THROUGH, false) .getResultList(); assertEquals(POST_COUNT, posts.size()); for(Post post : posts) { assertEquals(POST_COMMENT_COUNT, post.getComments().size()); assertEquals(TAG_COUNT, post.getTags().size()); }
Первый запрос JPQL определяет основные критерии фильтрации и извлекает объекты Post
вместе с соответствующими записями PostComment
.
Подсказка PASS_DISTINCT_THROUGH
запроса позволяет избежать передачи ключевого слова DISTINCT в инструкцию SQL и использовать его только для удаления дубликатов сущностей Java, вызванных объединенным результирующим набором “родитель-потомок”. Для получения более подробной информации о подсказке PASS_DISTINCT_THROUGH
запроса ознакомьтесь с этой статьей .
Теперь мы должны принести Опубликуйте
сущности вместе со связанными с ними Тегами
сущностями, и, благодаря контексту сохранения , Hibernate установит теги
коллекцию ранее извлеченных Записей
сущностей.
Круто, правда?
Вывод
Существует так много сообщений в блогах, видео, книг и ответов на форумах, предлагающих неправильное решение проблемы MultipleBagFetchException
Hibernate. Все эти ресурсы говорят вам, что использование Set
вместо List
является правильным способом избежать этого исключения.
Однако исключение MultipleBagFetchException
сообщает вам, что может быть сгенерировано декартово произведение, и в большинстве случаев это нежелательно при извлечении сущностей, поскольку это может привести к ужасным проблемам с производительностью доступа к данным.
Лучший способ получить несколько коллекций сущностей с помощью JPA и Hibernate-загрузить не более одной коллекции за раз, полагаясь на контекст сохранения Hibernate, гарантирующий, что в данный сеанс JPA EntityManager
или Hibernate одновременно может загружаться только один объект сущности
.