본문 바로가기
Java & Kotlin/Spring Data

[QueryDSL] oneToMany 관계에서 여러 개의 fetchJoin 사용하기

by heekng 2022. 6. 24.
반응형

oneToMany 관계에서 여러 개의 fetchJoin 사용하기

QueryDSL을 사용하면서 Data Jpa에서 해결하기 복잡한 쿼리를 쉽게 사용하고 있다.

하지만 하나의 엔티티가 두개 이상의 자식 엔티티에 oneToMany 관계로 구성되어있을 때 해당 자식 엔티티를 모두 fetchJoin하려 할 때 문제가 발생했다.

문제

현재 Home 엔티티는 Person과 Dog 엔티티와 OneToMany 연관관계로 구성되어있다.

@Test
void oneToManyFetchJoinTest() throws Exception {
    //given
    Home home1 = Home.builder()
            .name("home1")
            .build();
    em.persist(home1);
    Home home2 = Home.builder()
            .name("home2")
            .build();
    em.persist(home2);
    Home home3 = Home.builder()
            .name("home3")
            .build();
    em.persist(home3);
    Person person1 = Person.builder()
            .name("person1")
            .home(home1)
            .build();
    em.persist(person1);
    Person person2 = Person.builder()
            .name("person2")
            .home(home1)
            .build();
    em.persist(person2);
    Dog dog1 = Dog.builder()
            .name("dog1")
            .home(home1)
            .build();
    em.persist(dog1);
    Dog dog2 = Dog.builder()
            .name("dog2")
            .home(home1)
            .build();
    em.persist(dog2);
    em.flush();
    em.clear();
    //when
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    QHome qHome = QHome.home;
    QPerson qPerson = QPerson.person;
    QDog qDog = QDog.dog;

    List<Home> homes = queryFactory
            .selectFrom(qHome)
            .leftJoin(qHome.persons, qPerson)
            .fetchJoin()
            .leftJoin(qHome.dogs, qDog)
            .fetchJoin()
            .fetch();
    //then
    System.out.println("homes.size() = " + homes.size());
}

위와 같이 home을 조회하면서 person과 dog를 fetchJoin하여 가져오려 한다면

위와 같이 MultipleBagFetchException이 발생하며 여러 개의 fetchJoin이 불가하다는 오류메세지가 나타난다.

해결은?

우선 default_batch_fetch_size를 사용하였다.

# application.yml
spring:
  jpa:
    properties:
      hibernate.default_batch_fetch_size: 1000

default_batch_fetch_size는 컬렉션이나, 프록시 객체를 한꺼번에 설정한 size 만큼 IN 쿼리로 조회하도록 설정하는 옵션이다.
따라서 fetchJoin시에 default_batch_fetch_size설정값에 따라 쿼리 호출량을 1 : N 개가 아닌 1 : (N/1000) + 1회로 줄여 성능상의 이점을 가질 수 있다.

List<Home> homes = queryFactory
        .selectFrom(qHome)
        .fetch();
homes.stream()
        .map(Home::getPersons)
        .forEach(Hibernate::initialize);
homes.stream()
        .map(Home::getDogs)
        .forEach(Hibernate::initialize);

그리고 위와 같이 일반적인 조회 후, Hibernate.initialize() 메서드를 이용해 해당 자식 엔티티를 initialize한다.

Hibernate.initialize()란 LazyLoading되어 생성된 Proxy 객체를 Load해주는 메서드이다.

위처럼 코드를 수정하고 다시 조회한다면?

@Test
void oneToManyFetchJoinTest() throws Exception {
    //given
    Home home1 = Home.builder()
            .name("home1")
            .build();
    em.persist(home1);
    Home home2 = Home.builder()
            .name("home2")
            .build();
    em.persist(home2);
    Home home3 = Home.builder()
            .name("home3")
            .build();
    em.persist(home3);
    Person person1 = Person.builder()
            .name("person1")
            .home(home1)
            .build();
    em.persist(person1);
    Person person2 = Person.builder()
            .name("person2")
            .home(home1)
            .build();
    em.persist(person2);
    Dog dog1 = Dog.builder()
            .name("dog1")
            .home(home1)
            .build();
    em.persist(dog1);
    Dog dog2 = Dog.builder()
            .name("dog2")
            .home(home1)
            .build();
    em.persist(dog2);
    em.flush();
    em.clear();
    //when
    JPAQueryFactory queryFactory = new JPAQueryFactory(em);

    QHome qHome = QHome.home;

    List<Home> homes = queryFactory
            .selectFrom(qHome)
            .fetch();
    homes.stream()
            .map(Home::getPersons)
            .forEach(Hibernate::initialize);
    homes.stream()
            .map(Home::getDogs)
            .forEach(Hibernate::initialize);
    em.flush();
    em.clear();
    //then
    assertThat(homes.size()).isEqualTo(3);
    assertThat(homes.get(0).getPersons().size()).isEqualTo(2);
    assertThat(homes.get(0).getPersons().get(0).getName()).isEqualTo(person1.getName());
    assertThat(homes.get(0).getDogs().size()).isEqualTo(2);
    assertThat(homes.get(0).getDogs().get(0).getName()).isEqualTo(dog1.getName());
}
2022-06-24 16:50:09.452 DEBUG 48766 --- [           main] org.hibernate.SQL                        : select home0_.home_id as home_id1_1_, home0_.name as name2_1_ from home home0_
2022-06-24 16:50:09.464 DEBUG 48766 --- [           main] org.hibernate.SQL                        : select persons0_.home_id as home_id3_2_1_, persons0_.person_id as person_i1_2_1_, persons0_.person_id as person_i1_2_0_, persons0_.home_id as home_id3_2_0_, persons0_.name as name2_2_0_ from person persons0_ where persons0_.home_id in (?, ?, ?)
2022-06-24 16:50:09.472 DEBUG 48766 --- [           main] org.hibernate.SQL                        : select dogs0_.home_id as home_id3_0_1_, dogs0_.dog_id as dog_id1_0_1_, dogs0_.dog_id as dog_id1_0_0_, dogs0_.home_id as home_id3_0_0_, dogs0_.name as name2_0_0_ from dog dogs0_ where dogs0_.home_id in (?, ?, ?)

위와 같이 해당 fetchJoin이 필요한 Home 객체당 한번씩 쿼리를 전송하는 것이 아닌 in 절을 이용하여 하나의 쿼리로 모든 Home 객체의 Person을 받아온다.

마침

oneToMany 관계에서 다양한 fetchJoin 방법이 있겠지만, default_batch_fetch_size를 잘 조절할 수 있다면 이 방법이 가장 깔끔하다고 생각한다.

반응형