728x90
필자는 N+1 문제를 해결하기 위해 보통은 Fetch Join을 애용하고 있었다.
도토링 프로젝트를 개발하며, 여느때와 같이 Fetch Join을 여러번 사용했는 데 아래와 같은 에러를 마주했다.
org.hibernate.loader.MultipleBagFetchException: cannot simultaneously fetch multiple bags: [com.theZ.dotoring.app.mento.model.Mento.memberMajors, com.theZ.dotoring.app.mento.model.Mento.desiredFields]
MultipleBagFetchException이 뭘까??
이는 이름에서도 유추할 수 있듯이 Fetch Join을 여러번 사용하면 발생할 수 있는 에러이다.
구체적으로 어떤 상황에서 발생하는 것일까??
이를 알기 위해서는 Fetch Join을 사용할 때의 조건을 알아야한다.
Fetch Join의 조건은 아래와 같다.
- ManyToOne의 경우에는 무한정 Fetch Join이 가능
- OneToMany의 경우에는 1번만 Fetch Join이 가능
따라서, MultipleBagFetchException은 2개 이상의 OneToMany의 의 엔티티에 Fetch Join을 걸었을 때 발생한다!
현재 상황
아래는 현재 상황이다!
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Mento extends CommonEntity {
@Id
private Long mentoId;
@OneToOne(fetch = FetchType.LAZY)
private Profile profile;
@OneToMany(mappedBy = "mento")
private List<DesiredField> desiredFields = new ArrayList<>();
@OneToMany(mappedBy = "mento")
private List<MemberMajor> memberMajors = new ArrayList<>();
...
}
@Query("SELECT distinct M FROM Mento M JOIN FETCH M.profile JOIN FETCH M.memberMajors JOIN FETCH M.desiredFields WHERE M.mentoId in :mentoIds")
List<Mento> findMentosWithProfileAndFieldsAndMajorsUsingFetchJoinByMentoId(@Param("mentoIds") List<Long> mentoIds);
해결 방법
해결방법은 간단하다!
OneToMany 엔티티들 중 하나만 Fetch Join을 사용하면 된다!
그리고, N + 1 문제를 해결하기 위한 또 다른 방법인 batch_fetch_size 옵션을 활용하면 된다.
@Query("SELECT distinct M FROM Mento M JOIN FETCH M.profile JOIN FETCH M.memberMajors WHERE M.mentoId in :mentoIds")
List<Mento> findMentosWithProfileAndFieldsAndMajorsUsingFetchJoinByMentoId(@Param("mentoIds") List<Long> mentoIds);
- OneToMany의 경우에는 1번만 Fetch Join이 가능하기때문에, 더 많은 데이터를 가진 MemberMajors엔티티를 선택!
테스트
@Test
void findMentosWithProfileAndFieldsAndMajorsUsingFetchJoinByMentoId(){
List<Long> mentoIds = new ArrayList<>();
mentoIds.add(2L);
mentoIds.add(3L);
mentoIds.add(4L);
List<Mento> mentos = mentoRepository.findMentosWithProfileAndFieldsAndMajorsUsingFetchJoinByMentoId(mentoIds);
Assertions.assertThat(mentos.size()).isEqualTo(3);
Assertions.assertThat(mentos.get(0).getDesiredFields().stream().map(df -> df.getField().getFieldName()).collect(Collectors.toList()).get(0)).isEqualTo("진로");
Assertions.assertThat(mentos.get(0).getDesiredFields().stream().map(df -> df.getField().getFieldName()).collect(Collectors.toList()).get(1)).isEqualTo("개발_언어");
Assertions.assertThat(mentos.get(0).getDesiredFields().stream().map(df -> df.getField().getFieldName()).collect(Collectors.toList()).get(2)).isEqualTo( "공모전");
}
발생 쿼리
select
distinct mento0_.mento_id as mento_id1_7_0_,
profile1_.profile_id as profile_1_8_1_,
membermajo2_.member_major_id as member_m1_5_2_,
mento0_.created_at as created_2_7_0_,
mento0_.updated_at as updated_3_7_0_,
mento0_.grade as grade4_7_0_,
mento0_.introduction as introduc5_7_0_,
mento0_.mentoring_count as mentorin6_7_0_,
mento0_.mentoring_system as mentorin7_7_0_,
mento0_.nickname as nickname8_7_0_,
mento0_.profile_profile_id as profile11_7_0_,
mento0_.school as school9_7_0_,
mento0_.view_count as view_co10_7_0_,
profile1_.created_at as created_2_8_1_,
profile1_.updated_at as updated_3_8_1_,
profile1_.original_profile_name as original4_8_1_,
profile1_.saved_profile_name as saved_pr5_8_1_,
membermajo2_.major_name as major_na2_5_2_,
membermajo2_.menti_id as menti_id3_5_2_,
membermajo2_.mento_id as mento_id4_5_2_,
membermajo2_.mento_id as mento_id4_5_0__,
membermajo2_.member_major_id as member_m1_5_0__
from
mento mento0_
inner join
profile profile1_
on mento0_.profile_profile_id=profile1_.profile_id
inner join
member_major membermajo2_
on mento0_.mento_id=membermajo2_.mento_id
where
mento0_.mento_id in (
? , ? , ?
)
df.getField().getFieldName()
위 코드에서 배치를 이용한 select 쿼리가 나갈 것으로 예상 되었으나,
현재, 아래와 같이 PK를 fieldName으로 설계 하였기 때문에 추가적인 쿼리는 나기지 않는다!
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Field {
@Id
private String fieldName;
@OneToMany(mappedBy = "field")
private List<DesiredField> desiredFields = new ArrayList<>();
...
}
추후에 Field엔티티 관련 요구사항이 추가되 컬럼이 추가되고 이를 이용해야된다면, 배치를 이용한 select 쿼리가 나갈 것이다.
728x90
'Project Trouble Shooting > [Dotoring] 멘토링 어플리케이션' 카테고리의 다른 글
왜? 이미지가 찾아지지 않는 걸까?? - 로컬환경에서 외부 경로를 이용 (1) | 2023.08.21 |
---|---|
QueryDSL을 사용해 통계(Count)쿼리의 결과를 사용해서 정렬하기 (0) | 2023.08.15 |
데이터 무결성 지켜야지, 안 지킬거야?? (0) | 2023.08.07 |
식별자, 비식별자 관계는 뭐고 언제 사용해야하는 걸까?? (0) | 2023.08.04 |
엔티티의 통합과 분리의 기준이 뭐야?? (0) | 2023.08.04 |