Fetch Join을 여러번 했더니 MultipleBagFetchException이???

2023. 8. 16. 16:05프로젝트/[Dotoring] 멘토링 어플리케이션

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