본문 바로가기
개발/JPA

[JPA] JPQL fetch join / 연관된 entity 한번에 select / distinct로 엔티티 중복 제거 / fetch join 페이징 안 될 때 @BatchSize 설정

by Allonsy 2022. 4. 12.
반응형

fetch join을 설명하기전에 fetch type에 대해 복습을 해보자!

# FetchType

@ManyToOne, @OneToOne

FetchType이 기본이 EAGER다

- FetchType.EAGER

entity를 조회할 때 연관된 entity도 같이 조회해온다

이 경우 연관된 entity까지 select하는 쿼리가 나가야 하므로 성능이 좋지 않다

ex. member entity 조회 시 team entity까지 select 하므로 2번의 select 쿼리가 나간다

그래서 성능을 위해 모든 엔티티의 연관관계의 FetchType은 LAZY로 설정해두는 것이 좋다

- FetchType.LAZY

조회한 엔티티와 연관된 엔티티는 실제 사용 시에 조회해온다

ex. member 조회 -> member만 select

member.getTeam().getName() -> 이때 team select

# fetch join

모든 엔티티를 FetchType.LAZY로 설정해두면 연관된 엔티티를 조회해오지 않는다고 했다

그러면 연관된 엔티티를 같이 조회해와야하는 경우 어떻게 해야 할까?

JPQL에서 연관된 엔티티(또는 컬렉션)를 SQL 한번으로 함께 조회하려면 fetch join을 사용하면 된다

[문법 - join fetch]

[예제 코드]

String query = "select m from Member m join fetch m.team";
List<Member> result = em.createQuery(query, Member.class).getResultList();
for (Member member : result) {
    System.out.println("member = " + member.getUsername() +" , " + member.getTeam().getName());
}

[실제 생성되는 SQL]

select
    member0_.id as id1_0_0_,
    team1_.id as id1_3_1_,
    member0_.age as age2_0_0_,
    member0_.TEAM_ID as team_id5_0_0_,
    member0_.type as type3_0_0_,
    member0_.username as username4_0_0_,
    team1_.name as name2_3_1_ 
from
    Member member0_ 
inner join
    Team team1_ 
        on member0_.TEAM_ID=team1_.id

# fetch join 한계

 

1. fetch join 대상에는 별칭을 주지 말자! 연관된 엔티티 일부만 필터링해서 가져오는 것은 바람직한 사용법이 아님

 

2. 일대다 조인의 경우 연관된 엔티티를 모두 가져오므로 데이터 뻥튀기가 생긴다

ex. select t from Team t join fetch t.members;

팀A-멤버1,멤버2

팀A-멤버1,멤버2

팀B-멤버3

 

💡 distinct를 사용해서 해결

1. SQL에 distinct 추가

2. 애플리케이션에서 엔티티 중복 제거

 

[예제 코드]

String distinctQuery = "select distinct t from Team t join fetch t.members";
List<Team> distinctList = em.createQuery(distinctQuery, Team.class).getResultList();

[실제 생성되는 SQL]

select
            distinct team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as team_id5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID

 2. 컬렉션을 fetch join 하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다

일대다, 다대다 연관관계에서 fetch join을 하면 데이터가 뻥튀기 되고 페이징 API를 사용할 수 없다

 

[예제 코드]

String teamQuery = "select t from Team t join fetch t.members";
List<Team> teamList = em.createQuery(teamQuery, Team.class)
        .setFirstResult(0)
        .setMaxResults(2)
        .getResultList();

[경고]

WARN: HHH000104: firstResult/maxResults specified with collection fetch; applying in memory!

 

[실제 생성 되는 SQL]

select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.age as age2_0_1_,
            members1_.TEAM_ID as team_id5_0_1_,
            members1_.type as type3_0_1_,
            members1_.username as username4_0_1_,
            members1_.TEAM_ID as team_id5_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID

=> 페이징 관련 구문을 찾아볼 수 없다

이는 SQL로 모든 데이터를 조회해온 후에 애플리케이션 메모리에서 페이징을 하기 때문이다

성능에 매우 안 좋으므로 이렇게 하지 말자!

 

💡해결 방안

fetch join을 하지 않고, BatchSize 사용 - IN 쿼리를 만들어서 한번엔 연관된 엔티티도 조회해옴, 페이징 가능

1. @BatchSize 어노테이션 사용

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

2. 글로벌 설정으로 batch size 설정(persistance.xml)

<property name="hibernate.default_batch_fetch_size" value="100"/>

[예제 코드]

String teamQuery = "select t from Team t";
List<Team> teamList = em.createQuery(teamQuery, Team.class)
        .setFirstResult(0)
        .setMaxResults(2)
        .getResultList();

 

[실제 생성되는 SQL]

    /* load one-to-many jpql.Team.members */ 
    select
        members0_.TEAM_ID as team_id5_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.age as age2_0_0_,
        members0_.TEAM_ID as team_id5_0_0_,
        members0_.type as type3_0_0_,
        members0_.username as username4_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )

 

[참고] fetch와 관련하여 더 자세한 내용은 하이버네이트 공식문서 11. Fetching 을 읽어보시길 추천!

https://docs.jboss.org/hibernate/orm/6.0/userguide/html_single/Hibernate_User_Guide.html#fetching

 

Hibernate ORM 6.0.0.Final User Guide

Fetching, essentially, is the process of grabbing data from the database and making it available to the application. Tuning how an application does fetching is one of the biggest factors in determining how an application will perform. Fetching too much dat

docs.jboss.org

 

인프런 - 자바 ORM 표준 JPA 프로그래밍 - 기본편(김영한 강사님) 강의를 수강하며 정리한 글입니다 :)

반응형

댓글