Рубрики
Без рубрики

Лучший способ исправить исключение Hibernate MultipleBagFetchException

Узнайте, как лучше всего исправить исключение Hibernate MultipleBagFetchException, вызванное извлечением нескольких ассоциаций списков вместе с их родительской сущностью.

Автор оригинала: Vlad Mihalcea.

Вступление

Если вы используете Hibernate в течение некоторого времени, есть большая вероятность, что вы столкнулись с проблемой MultipleBagFetchException :

организация.спящий режим.загрузчик. Исключение MultipleBagFetchException: не удается одновременно извлечь несколько пакетов

В этой статье мы рассмотрим причину, по которой Hibernate выдает исключение MultipleBagFetchException , а также лучший способ решения этой проблемы.

Модель предметной области

Давайте рассмотрим, что наше приложение определяет три сущности: Сообщение , Комментарий к сообщению , и Тег , которые связаны, как показано на следующей диаграмме:

Что нас больше всего интересует в этой статье, так это то, что Сообщение сущность определяет двунаправленную @OneToMany связь с Комментарием к сообщению дочерней сущностью, а также однонаправленную @ManyToMany ассоциация с Тегом сущностью.

@OneToMany(
    mappedBy = "post", 
    cascade = CascadeType.ALL, 
    orphanRemoval = true
)
private List comments = 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, а также все связанные с ними Сообщения и Тег сущности, мы бы написали запрос, подобный следующему:

List posts = 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 Set comments = 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), как показано в следующем тестовом примере:

List posts = 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-запроса:

List posts = 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 одновременно может загружаться только один объект сущности .