Перейти к основному содержимому

Руководство по MultipleBagFetchException в Hibernate

· 8 мин. чтения

1. Обзор

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

Мы создадим домен простого музыкального приложения, чтобы продемонстрировать каждое из решений.

2. Что такое сумка в спящем режиме?

Bag, как и List , представляет собой коллекцию, которая может содержать повторяющиеся элементы. Однако это не по порядку. Более того, Bag — это термин Hibernate , который не является частью Java Collections Framework.

Учитывая более раннее определение, стоит подчеркнуть, что и List , и Bag используют java.util.List . Хотя в Hibernate оба трактуются по-разному. Чтобы отличить Bag от List , давайте посмотрим на него в реальном коде.

Сумка:

// @ any collection mapping annotation
private List<T> collection;

Список : _

// @ any collection mapping annotation
@OrderColumn(name = "position")
private List<T> collection;

3. Причина MultipleBagFetchException

Одновременная добыча двух или более сумок на объекте может сформировать декартово произведение. Поскольку у Bag нет порядка, Hibernate не сможет сопоставить правильные столбцы с нужными объектами. Следовательно, в этом случае он выдает MultipleBagFetchException .

Давайте рассмотрим несколько конкретных примеров, которые приводят к MultipleBagFetchException.

Для первого примера попробуем создать простую сущность с двумя сумками, обе с типом « `нетерпеливая выборка». Художник может быть хорошим примером. Он может иметь коллекцию песен и предложений` .

Учитывая это, давайте создадим сущность Artist :

@Entity
class Artist {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String name;

@OneToMany(mappedBy = "artist", fetch = FetchType.EAGER)
private List<Song> songs;

@OneToMany(mappedBy = "artist", fetch = FetchType.EAGER)
private List<Offer> offers;

// constructor, equals, hashCode
}

Если мы попытаемся запустить тест, мы немедленно столкнемся с MultipleBagFetchException , и он не сможет построить Hibernate SessionFactory . Сказав это, давайте не будем этого делать.

Вместо этого давайте преобразуем один или оба типа выборки коллекций в ленивый:

@OneToMany(mappedBy = "artist")
private List<Song> songs;

@OneToMany(mappedBy = "artist")
private List<Offer> offers;

Теперь мы сможем создать и запустить тест. Хотя, если мы попытаемся получить обе эти коллекции сумок одновременно, это все равно приведет к 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 , поэтому давайте продолжим работу с двумя другими объектами.

Во-первых, давайте посмотрим на сущность альбома :

@Entity
class Album {

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;

private String name;

@OneToMany(mappedBy = "album")
private List<Song> songs;

@ManyToMany(mappedBy = "followingAlbums")
private Set<Follower> 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 List<Playlist> playlists;

@OneToMany(mappedBy = "user", cascade = CascadeType.PERSIST)
@OrderColumn(name = "arrangement_index")
private List<FavoriteSong> favoriteSongs;

// constructor, equals, hashCode
}

Пользователь может создать множество плейлистов . Кроме того, у пользователя есть отдельный список для избранных песен, в котором их порядок основан на индексе аранжировки.

6. Обходной путь: использование набора в одном запросе JPQL

Прежде всего, давайте подчеркнем, что этот подход будет генерировать декартово произведение, что делает его простым обходным путем. Это потому, что мы будем получать две коллекции одновременно в одном запросе JPQL. Напротив, нет ничего плохого в использовании Set . Это подходящий выбор, если нам не нужно, чтобы наша коллекция имела порядок или какие-либо повторяющиеся элементы. **** ``

Чтобы продемонстрировать этот подход, давайте сошлемся на сущность Album из нашей модели предметной области.

Сущность Альбом имеет две коллекции: песни и подписчики . Сборник песен представляет собой сумку. Однако для подписчиков мы используем набор. При этом мы не столкнемся с 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());
}

Как мы видим, мы успешно получили альбом . Это потому, что только список песен - Сумка . С другой стороны, набор последователей — это Set .

Кстати , стоит подчеркнуть, что мы используем QueryHints.HINT_PASS_DISTINCT_THROUGH. Поскольку мы используем запрос сущности JPQL, это предотвращает включение ключевого слова DISTINCT в фактический запрос SQL. Таким образом, мы будем использовать эту подсказку запроса и для остальных подходов.

7. Обходной путь: использование списка в одном запросе JPQL

Как и в предыдущем разделе, это также будет генерировать декартово произведение, что может привести к проблемам с производительностью . Опять же, нет ничего плохого в использовании List , Set или Bag для типа данных. Цель этого раздела — продемонстрировать, что Hibernate может извлекать коллекции одновременно, если существует не более одной коллекции типа Bag.

Для этого подхода давайте воспользуемся сущностью User из нашей модели предметной области.

Как упоминалось ранее, у пользователя есть две коллекции: плейлисты и избранные песни . Списки воспроизведения не имеют определенного порядка, что делает их сборником сумок. Однако для List of FavoriteSongs его порядок зависит от того, как его упорядочивает Пользователь . Если мы внимательно посмотрим на объект FavoriteSong , то увидим, что это позволяет сделать это благодаря свойству layoutIndex .

Опять же, используя один запрос JPQL, давайте попробуем проверить, сможем ли мы получить всех пользователей, одновременно извлекая как коллекции плейлистов , так и избранные песни .

Для демонстрации создадим интеграционный тест:

@Test
public void whenFetchingOneBagAndOneList_thenRetrieveSuccess() {
String jpql = "SELECT DISTINCT user FROM User user "
+ "LEFT JOIN FETCH user.playlists "
+ "LEFT JOIN FETCH user.favoriteSongs ";

List<User> users = entityManager.createQuery(jpql, User.class)
.setHint(QueryHints.HINT_PASS_DISTINCT_THROUGH, false)
.getResultList();

assertEquals(3, users.size());
}

Из утверждения мы видим, что мы успешно получили всех пользователей. Более того, мы не столкнулись с MultipleBagFetchException . Это потому, что, несмотря на то, что мы получаем две коллекции одновременно, только плейлисты представляют собой коллекцию сумок.

8. Идеальное решение: использование нескольких запросов

Из предыдущих обходных путей мы видели использование одного запроса JPQL для одновременного извлечения коллекций. К сожалению, он генерирует декартово произведение. Мы знаем, что это не идеально. Итак, давайте решим MultipleBagFetchException , не жертвуя производительностью.

Предположим, мы имеем дело с сущностью, которая имеет более одной коллекции сумок. В нашем случае это сущность Artist . Имеет две коллекции сумок: песни и предложения .

В этой ситуации мы даже не сможем получить обе коллекции одновременно с помощью одного запроса JPQL. Это приведет к MultipleBagFetchException . Вместо этого давайте разделим его на два запроса JPQL.

При таком подходе мы ожидаем успешного извлечения обеих коллекций сумок по одной.

Снова, в последний раз , давайте быстро создадим интеграционный тест для поиска всех исполнителей:

@Test
public void whenUsingMultipleQueries_thenRetrieveSuccess() {
String jpql = "SELECT DISTINCT artist FROM Artist artist "
+ "LEFT JOIN FETCH artist.songs ";

List<Artist> artists = 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 .