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 .