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

Руководство по MultipleBagFetchException в спящем режиме

Подробнее об исключении MultipleBagFetchException.

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

1. Обзор

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

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

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

Сумка, аналогичная списку , – это коллекция, которая может содержать повторяющиеся элементы. Однако это не в порядке вещей. Более того, сумка-это Hibernate термин и не является частью фреймворка коллекций Java.

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

Сумка:

// @ any collection mapping annotation
private List collection;

A Список :

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

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 List songs;

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

    // constructor, equals, hashCode
}

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

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

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

@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 List songs;

    @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 List playlists;

    @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 ";

    List users = 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 ";

    List 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 .