1. Обзор
В этом уроке мы поговорим об исключении MultipleBagFetchException . Мы начнем с необходимых терминов для понимания, а затем рассмотрим некоторые обходные пути, пока не достигнем идеального решения.
Мы создадим простой домен музыкальных приложений, чтобы продемонстрировать каждое из решений.
2. Что такое ошибка в спящем режиме?
Сумка, аналогичная списку , – это коллекция, которая может содержать повторяющиеся элементы. Однако это не в порядке вещей. Более того, сумка-это Hibernate термин и не является частью фреймворка коллекций Java.
Учитывая более раннее определение, стоит подчеркнуть, что и List , и Bag используют java.util.Список . Хотя в спящем режиме к обоим относятся по-разному. Чтобы отличить сумку от Списка , давайте рассмотрим ее в реальном коде.
Сумка:
// @ any collection mapping annotation private Listcollection;
A Список :
// @ any collection mapping annotation @OrderColumn(name = "position") private Listcollection;
3. Причина исключения MultipleBagFetchException
Извлечение двух или более пакетов одновременно в Сущности может сформировать декартово произведение. Поскольку пакет не имеет порядка, Hibernate не сможет сопоставить правильные столбцы с правильными сущностями. Следовательно, в этом случае он создает исключение MultipleBagFetchException .
Давайте приведем несколько конкретных примеров, которые приводят к исключению MultipleBagFetchException.
В первом примере давайте попробуем создать простую сущность, которая имеет 2 пакета и оба с типом |/eager fetch. Хорошим примером может быть Художник . Он может иметь коллекцию песен и предложений .
Учитывая это, давайте создадим сущность Artist :
@Entity class Artist { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToMany(mappedBy = "artist", fetch = FetchType.EAGER) private Listsongs; @OneToMany(mappedBy = "artist", fetch = FetchType.EAGER) private List offers; // constructor, equals, hashCode }
Если мы попытаемся запустить тест, мы немедленно столкнемся с MultipleBagFetchException , и он не сможет построить Hibernate SessionFactory . Сказав это, давайте не будем этого делать.
Вместо этого давайте преобразуем один или оба типа выборки коллекций в lazy:
@OneToMany(mappedBy = "artist") private Listsongs; @OneToMany(mappedBy = "artist") private List offers;
Теперь мы сможем создать и запустить тест. Хотя, если мы попытаемся извлечь обе эти коллекции bag одновременно, это все равно приведет к исключению MultipleBagFetchException .
4. Имитируйте исключение MultipleBagFetchException
В предыдущем разделе мы рассмотрели причины исключения MultipleBagFetchException. Здесь давайте проверим эти утверждения, создав интеграционный тест.
Для простоты давайте использовать объект Artist , который мы ранее создали.
Теперь давайте создадим интеграционный тест и попробуем получить оба песни и предложения одновременно, используя JPQL:
@Test public void whenFetchingMoreThanOneBag_thenThrowAnException() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { String jpql = "SELECT artist FROM Artist artist " + "JOIN FETCH artist.songs " + "JOIN FETCH artist.offers "; entityManager.createQuery(jpql); }); final String expectedMessagePart = "MultipleBagFetchException"; final String actualMessage = exception.getMessage(); assertTrue(actualMessage.contains(expectedMessagePart)); }
Из утверждения мы столкнулись с IllegalArgumentException , которое имеет первопричину MultipleBagFetchException .
5. Модель предметной области
Прежде чем перейти к возможным решениям, давайте рассмотрим необходимые модели предметной области, которые мы будем использовать в качестве ссылки позже.
Предположим, мы имеем дело с доменом музыкальных приложений. Учитывая это, давайте сосредоточимся на определенных сущностях: Альбом, Исполнитель, и Пользователь.
Мы уже видели сущность Artist , поэтому давайте вместо этого перейдем к двум другим сущностям.
Во-первых, давайте посмотрим на сущность Album :
@Entity class Album { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToMany(mappedBy = "album") private Listsongs; @ManyToMany(mappedBy = "followingAlbums") private Set followers; // constructor, equals, hashCode }
Альбом имеет коллекцию песен и в то же время может иметь набор подписчиков .
Далее, вот сущность User :
@Entity class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; private String name; @OneToMany(mappedBy = "createdBy", cascade = CascadeType.PERSIST) private Listplaylists; @OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST) @OrderColumn(name = "arrangement_index") private List favoriteSongs; // constructor, equals, hashCode }
Пользователь может создавать множество плейлистов . Кроме того, у Пользователя есть отдельный Список для любимых песен , в котором его порядок основан на индексе аранжировки.
6. Обходной путь: Использование набора в одном запросе JPQL
Прежде всего, давайте подчеркнем, что этот подход будет генерировать декартово произведение, что делает это простым обходным путем. Это потому, что мы будем получать две коллекции одновременно в одном запросе JPQL. Напротив, нет ничего плохого в использовании Set . Это подходящий выбор, если нам не нужно, чтобы в нашей коллекции был заказ или какие-либо дублированные элементы.
Чтобы продемонстрировать этот подход, давайте обратимся к сущности Album из нашей модели домена.
Альбом сущность имеет две коллекции: песни и подписчики . Коллекция песен имеет тип сумка. Однако для подписчиков мы используем Set. С учетом сказанного, мы не столкнемся с MultipleBagFetchException , даже если мы попытаемся извлечь обе коллекции одновременно.
Используя интеграционный тест, давайте попробуем получить Альбом по его идентификатору, извлекая обе его коллекции в одном запросе JPQL:
@Test public void whenFetchingOneBagAndSet_thenRetrieveSuccess() { String jpql = "SELECT DISTINCT album FROM Album album " + "LEFT JOIN FETCH album.songs " + "LEFT JOIN FETCH album.followers " + "WHERE album.id = 1"; Query query = entityManager.createQuery(jpql) .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false); assertEquals(1, query.getResultList().size()); }
Как мы видим, мы успешно извлекли Альбом . Это потому, что только список песен является сумкой . С другой стороны, коллекция подписчиков является Набором .
В качестве примечания стоит отметить, что мы используем подсказки Query.HINT_PASS_DISTINCT_THROUGH. Поскольку мы используем запрос JPQL сущности, он предотвращает включение ключевого слова DISTINCT в фактический SQL-запрос. Таким образом, мы будем использовать эту подсказку запроса и для остальных подходов.
7. Обходной путь: Использование списка в одном запросе JPQL
Как и в предыдущем разделе, это также приведет к созданию декартова произведения, что может привести к проблемам с производительностью . Опять же, нет ничего плохого в использовании List , Set, или Bag для типа данных. Цель этого раздела-дополнительно продемонстрировать, что Hibernate может извлекать коллекции одновременно, если существует не более одного типа Bag.
Для этого подхода давайте использовать сущность User из нашей модели домена.
Как упоминалось ранее, у Пользователя есть две коллекции: плейлисты и любимые песни . Плейлисты не имеют определенного порядка, что делает их коллекцией сумок. Однако для Списка любимых песен его порядок зависит от того , как Пользователь организует его . Если мы внимательно посмотрим на сущность Любимая песня , свойство Индекс аранжировки позволило это сделать.
Опять же, используя один запрос JPQL, давайте попробуем проверить, сможем ли мы получить всех пользователей, одновременно извлекая обе коллекции плейлистов и избранных песен .
Чтобы продемонстрировать это, давайте создадим интеграционный тест:
@Test public void whenFetchingOneBagAndOneList_thenRetrieveSuccess() { String jpql = "SELECT DISTINCT user FROM User user " + "LEFT JOIN FETCH user.playlists " + "LEFT JOIN FETCH user.favoriteSongs "; Listusers = entityManager.createQuery(jpql, User.class) .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) .getResultList(); assertEquals(3, users.size()); }
Из утверждения мы видим, что мы успешно восстановили всех пользователей. Более того, мы не столкнулись с MultipleBagFetchException . Это потому, что, хотя мы получаем две коллекции одновременно, только плейлисты являются коллекцией сумок.
8. Идеальное Решение: Использование Нескольких Запросов
Из предыдущих обходных путей мы видели использование одного запроса JPQL для одновременного извлечения коллекций. К сожалению, он генерирует декартово произведение. Мы знаем, что это не идеально. Итак, давайте решим исключение MultipleBagFetchException без необходимости жертвовать производительностью.
Предположим, мы имеем дело с сущностью, у которой более одной коллекции сумок. В нашем случае это Исполнитель сущность. У него есть две коллекции сумок: песни и предложения .
Учитывая эту ситуацию, мы даже не сможем получить обе коллекции одновременно, используя один запрос JPQL. Это приведет к исключению MultipleBagFetchException . Вместо этого давайте разделим его на два запроса JPQL.
При таком подходе мы ожидаем, что обе коллекции сумок будут успешно собраны по одной за раз.
Опять же, в последний раз , давайте быстро создадим интеграционный тест для поиска всех художников:
@Test public void whenUsingMultipleQueries_thenRetrieveSuccess() { String jpql = "SELECT DISTINCT artist FROM Artist artist " + "LEFT JOIN FETCH artist.songs "; Listartists = entityManager.createQuery(jpql, Artist.class) .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) .getResultList(); jpql = "SELECT DISTINCT artist FROM Artist artist " + "LEFT JOIN FETCH artist.offers " + "WHERE artist IN :artists "; artists = entityManager.createQuery(jpql, Artist.class) .setParameter("artists", artists) .setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false) .getResultList(); assertEquals(2, artists.size()); }
Из теста мы сначала извлекли всех исполнителей, извлекая его коллекцию песен .
Затем мы создали еще один запрос для получения предложений художников .
Используя этот подход, мы избежали Исключение MultipleBagFetchException а также формирование декартова произведения.
9. Заключение
В этой статье мы подробно рассмотрели MultipleBagFetchException . Мы обсудили необходимую лексику и причины этого исключения. Затем мы смоделировали это. После этого мы поговорили о домене простого музыкального приложения, чтобы иметь различные сценарии для каждого из наших обходных путей и идеального решения. Наконец, мы создали несколько интеграционных тестов для проверки каждого из подходов.
Как всегда, полный исходный код статьи доступен на GitHub .