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가 동시에 컬렉션을 가져올 수 있음을 더 설명하는 것입니다.
이 접근 방식을 위해 도메인 모델 의 사용자 엔터티를 사용하겠습니다 .
앞서 언급했듯이 사용자 에게는 재생 List 과 favoriteSongs의 두 가지 컬렉션이 있습니다. 재생 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 에 대해 자세히 살펴 보았습니다 . 필요한 어휘와이 예외의 원인에 대해 논의했습니다. 그런 다음 시뮬레이션했습니다. 그 후, 우리는 각 해결 방법과 이상적인 솔루션에 대해 서로 다른 시나리오를 갖는 간단한 음악 앱의 도메인에 대해 이야기했습니다. 마지막으로 각 접근 방식을 확인하기 위해 몇 가지 통합 테스트를 설정했습니다.