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
를 잘 조절할 수 있다면 이 방법이 가장 깔끔하다고 생각한다.
'Java & Kotlin > Spring Data' 카테고리의 다른 글
[JPA] illegally attempted to associate proxy with two open Sessions 에러 (0) | 2022.07.01 |
---|---|
[JPA] Cascade persist와 연관관계 (0) | 2022.06.24 |
[QueryDSL] gradle querydsl 설정하기 (0) | 2022.05.15 |
[QueryDSL] 페이징 연동하기 (0) | 2022.05.15 |
[QueryDSL] Spring Data JPA와 QueryDSL 사용하기 (0) | 2022.05.15 |
[QueryDSL] 벌크연산과 SQL Funtion (0) | 2022.05.15 |