ABOUT ME

나의 개발 여정들의 기록들... https://github.com/hwangdaesun

Today
Yesterday
Total
  • Fetch Join을 여러번 했더니 MultipleBagFetchException이???
    Project Trouble Shooting/[Dotoring] 멘토링 어플리케이션 2023. 8. 16. 16:05
    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
Designed by Tistory.