1. 개요

이 예제에서는 MultipleBagFetchException 에 대해 이야기 할 것  입니다. 이해하는 데 필요한 용어로 시작한 다음 이상적인 솔루션에 도달 할 때까지 몇 가지 해결 방법을 살펴 보겠습니다.

각 솔루션을 보여주기 위해 간단한 음악 앱의 도메인을 만들 것입니다.

2. Hibernate에서 Bag이란 무엇입니까?

List 와 유사한 Bag  은 중복 요소를 포함 할 수있는 컬렉션입니다. 그러나 그것은 순서가 아닙니다. 게다가 Bag은 Hibernate 용어이며 Java Collections Framework의 일부가 아닙니다.

이전 정의를 고려할 때 List 와 Bag 모두 java.util.List를 사용 한다는 점을 강조 할 가치가 있습니다. Hibernate에서는 둘 다 다르게 취급됩니다. Bag과 List 를 구별하기 위해 실제 코드에서 살펴 보겠습니다.

가방:

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

List :

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

3. MultipleBagFetchException의 원인

엔티티 에서 동시에 두 개 이상의 가방을 가져 오면 카티 전 곱이 형성 될 수 있습니다. Bag에는 주문이 없기 때문에 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<Song> songs;

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

    // constructor, equals, hashCode
}

테스트를 실행하려고하면  즉시 MultipleBagFetchException 이 발생하고  Hibernate SessionFactory 를 빌드 할 수 없습니다 . 그렇다고해서 이렇게하지 말자.

대신 컬렉션의 페치 유형 중 하나 또는 둘 모두를 lazy로 변환 해 보겠습니다.

@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 엔터티를 보았 으므로 대신 다른 두 엔터티를 진행해 보겠습니다.

먼저 Album 엔티티를 살펴 보겠습니다 .

@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
}

사용자가 많은 만들 수 있습니다 재생 List을 . 또한 사용자순서가 정렬 색인을 기반으로 하는 별도의 favoriteSongs List가지고 있습니다.

6. 해결 방법 : 단일 JPQL 쿼리에서 집합 사용

무엇보다 먼저, 이 접근 방식이 데카르트 곱을 생성하여이를 단순한 해결 방법으로 만든다는 점을 강조합시다  . 단일 JPQL 쿼리에서 동시에 두 개의 컬렉션을 가져올 것이기 때문입니다.  대조적으로, Set 을 사용하는 것은 잘못된 것이 아닙니다  . 컬렉션에 순서 나 중복 된 요소가 필요하지 않은 경우 적절한 선택입니다. 

이 접근 방식을 보여주기 위해 도메인 모델에서 Album 엔터티를 참조하겠습니다 . 

앨범  : 개체가이 컬렉션이 노래추종자 . 노래 모음은   가방 유형입니다. 그러나  추종자들  에게는 세트를  사용하고  있습니다. 즉, 두 컬렉션을 동시에 가져 오려고해도 MultipleBagFetchException이  발생하지 않습니다  .

통합 테스트를 사용하여 단일 JPQL 쿼리에서 두 컬렉션을 모두 가져 오는 동안 ID Album 을 검색해 보겠습니다 .

@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());
}

보시다시피 Album 을 성공적으로 검색했습니다 . 노래 List 만 Bag 이기 때문 입니다. 반면에, 수집 추종자 A는 세트 .

참고로 QueryHints.HINT_PASS_DISTINCT_THROUGH 를 사용하고 있다는 점을 강조 할 가치가 있습니다  엔티티 JPQL 쿼리를 사용하기 때문에   실제 SQL 쿼리에 DISTINCT 키워드가 포함되지 않습니다. 따라서 나머지 접근 방식에도이 쿼리 힌트를 사용합니다. 

7. 해결 방법 : 단일 JPQL 쿼리에서 List 사용

이전 섹션과 유사하게  이것은 성능 문제로 이어질 수있는 데카르트 곱을 생성합니다 . 다시 말하지만,  데이터 유형에 대해 List , Set 또는 Bag을 사용하는 데 아무런 문제가 없습니다  . 이 섹션의 목적은 Bag 유형이 하나만있는 경우 Hibernate가 동시에 컬렉션을 가져올 수 있음을 더 설명하는 것입니다.

이 접근 방식을 위해  도메인 모델 사용자 엔터티를 사용하겠습니다  .

앞서 언급했듯이  사용자  에게는 재생 ListfavoriteSongs의 두 가지 컬렉션이 있습니다. 재생 List  이 가방 컬렉션을 만드는 순서를 정의하지 아니했다. 그러나  즐겨 찾기 List  의 경우  순서는 사용자가 정렬 하는 방식에 따라 다릅니다 . 우리가 자세히 보면 FavoriteSong의 실체는  arrangementIndex의  속성은 가능 그렇게했다.

다시 한 번, 단일 JPQL 쿼리를 사용하여 재생 List  과  favoriteSongs의 컬렉션을 동시에 가져 오는 동안 모든 사용자를 검색 할 수 있는지 확인해 보겠습니다   .

시연을 위해 통합 테스트를 만들어 보겠습니다.

@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 은 발생하지 않았습니다  . 두 개의 컬렉션을 동시에 가져 오지만 재생 List 가방 컬렉션 이기 때문  입니다.

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<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에서 사용할 수 있습니다 .