FETCH JOIN

XingXi·2024년 1월 8일
0

JPA

목록 보기
21/23
post-thumbnail

🍕 Reference

자바 ORM 표준 JPA 프로그래밍 : 교보문고
자바 ORM 표준 JPA 프로그래밍 - 기본편 : 인프런

  • fetch : 가져오다

Member

@Entity
public class Member
{
    @Id @GeneratedValue
    private Long id;
    private String username;

    private int age;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "TEAM_ID")
    private Team team;

    @Embedded
    private Address address;

    @Enumerated(EnumType.STRING)
    private MemberType memberType;

Team

@Entity
public class Team
{
    @Id @GeneratedValue
    private Long id;

    private String name;

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

FETCH JOIN

JPQL 에서 연관관계에 있는 컬럼 조회 시 연관관계 엔티티 까지 영속화하는 JOIN

  • 연관관계 Mapping 에서 즉시 로딩 FetchType.EAGER 와 같은 효과를 지닌다.
  • 지연로딩으로 선언된 연관관계라도 즉시 영속화 한다.
  • JPQL 을 사용하여 FetchType.EAGER 연관관계 컬럼을 조회 시 발생하는 N+1문제를 해결한다,

N + 1 이 발생하는 경우

main

        try
        {
            Address address = new Address("1","1","1");

            Team teamA = new Team();
            teamA.setName("A");
            em.persist(teamA);

            Team teamB = new Team();
            teamB.setName("B");
            em.persist(teamB);

            Member member1 = new Member();
...
            em.persist(member1);


            Member member2 = new Member();
...
            em.persist(member2);


            Member member3 = new Member();
...
            em.persist(member3);

            em.flush();
            em.clear();

            String query = "select m from Member m";
            List<Member> result = em.createQuery(query,Member.class)
                    .getResultList();

            for (Member m : result) {
                System.out.println("Member : "+m.getUsername() + "| TEAM : "+m.getTeam().getName());
            }

결과

Hibernate: 
    /* select
        m 
    from
        Member m */ select
            member0_.id as id1_0_,
            member0_.city as city2_0_,
            member0_.street as street3_0_,
            member0_.zipcode as zipcode4_0_,
            member0_.age as age5_0_,
            member0_.memberType as memberTy6_0_,
            member0_.TEAM_ID as TEAM_ID8_0_,
            member0_.username as username7_0_ 
        from
            Member member0_
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Member : 회원1| TEAM : A
Member : 회원2| TEAM : A
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
Member : 회원3| TEAM : B

Member에서 Team의 연관관계를 LazyLoading으로 설정하였다.
1. Member 만 조회
2. Member 에서 getTeam().getName() 으로 Proxy 객체 초기화 진행
3. nameA인 팀 초기화를 위해 Query 발생
4. nameA인 팀 객체가 영속화 되어 있기 때문에 쿼리 발생 x
5. nameB인 팀은 영속화 되어 있지 않기 때문에 영속성 초기화를 위해 Query 발생
N+1 이 발생한다.

FETCH JOIN 사용

이번에는 FETCH JOIN을 사용해 보자

main : 위의 코드에서 쿼리 부분만 변경

String query = "select m from Member m join fetch m.team";

결과

Hibernate: 
    /* select
        m 
    from
        Member m 
    join
        fetch m.team */ select
            member0_.id as id1_0_0_,
            team1_.id as id1_3_1_,
            member0_.city as city2_0_0_,
            member0_.street as street3_0_0_,
            member0_.zipcode as zipcode4_0_0_,
            member0_.age as age5_0_0_,
            member0_.memberType as memberTy6_0_0_,
            member0_.TEAM_ID as TEAM_ID8_0_0_,
            member0_.username as username7_0_0_,
            team1_.name as name2_3_1_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id
Member : 회원1| TEAM : A
Member : 회원2| TEAM : A
Member : 회원3| TEAM : B

한번의 쿼리에 연관된 Team 데이터를 모두 들고 온다.
Team과의 연관관계가 LazyLoading임에도 한꺼번에 들고 온다.

FETCH JOIN ( OneToMany 에서 조회 : Collection )

결과 값이 CollectionFETCH JOIN 조회
main

String query = "select t from Team t join fetch t.members where t.name = 'A'";
List<Team> result = em.createQuery(query,Team.class).getResultList();
for (Team t : result) 
{ System.out.println("Member : "+t.getMembers() + "| TEAM : "+t.getName()); }

result

Hibernate: 
    /* select
        t 
    from
        Team t 
    join
        fetch t.members 
    where
        t.name = 'A' */ select
            team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.city as city2_0_1_,
            members1_.street as street3_0_1_,
            members1_.zipcode as zipcode4_0_1_,
            members1_.age as age5_0_1_,
            members1_.memberType as memberTy6_0_1_,
            members1_.TEAM_ID as TEAM_ID8_0_1_,
            members1_.username as username7_0_1_,
            members1_.TEAM_ID as TEAM_ID8_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID 
        where
            team0_.name='A'
Member : [Member{id=3, username='회원1', age=20, address=org.example.entity.Address@37c36608}, Member{id=4, username='회원2', age=20, address=org.example.entity.Address@5d497a91}]| TEAM : A
Member : [Member{id=3, username='회원1', age=20, address=org.example.entity.Address@37c36608}, Member{id=4, username='회원2', age=20, address=org.example.entity.Address@5d497a91}]| TEAM : A
  • TeamnameA인 조건을 걸었지만 2개의 결과가 출력 된다.
    Query의 결과가 테이블에서는 TeamnameA 에 해당 되는 Member가 2개 임으로 2개가 출력이 된다.
  • 중복 제거를 DISTINCT 를 추가하고 엔티티 중복 제거를 수행한다.
    ( SQL 에서는 DISTINCT는 완전히 값들이 같아야 중복 제거를 한다.
    JPQL 의 DISTINCT같은 식별자를 가진 Entity 가 발견 될 경우 추가적으로 제거 작업을 수행한다. )

FETCH JOIN ( OneToMany 에서 조회 : Collection + DISTINCT )

main : distinct 추가

String query = "select distinct t from Team t join fetch t.members where t.name = 'A'";

결과

Hibernate: 
    /* select
        distinct t 
    from
        Team t 
    join
        fetch t.members 
    where
        t.name = 'A' */ select
            distinct team0_.id as id1_3_0_,
            members1_.id as id1_0_1_,
            team0_.name as name2_3_0_,
            members1_.city as city2_0_1_,
            members1_.street as street3_0_1_,
            members1_.zipcode as zipcode4_0_1_,
            members1_.age as age5_0_1_,
            members1_.memberType as memberTy6_0_1_,
            members1_.TEAM_ID as TEAM_ID8_0_1_,
            members1_.username as username7_0_1_,
            members1_.TEAM_ID as TEAM_ID8_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID 
        where
            team0_.name='A'
Member : [Member{id=3, username='회원1', age=20, address=org.example.entity.Address@1c8f6a90}, Member{id=4, username='회원2', age=20, address=org.example.entity.Address@3050ac2f}]| TEAM : A

FETCH JOIN 주의 사항

1. fetch Join 은 하나의 컬렉션에만 가능하다.

2. ALIAS 설정 불가능

  • 하이버네이트 구현체에는 사용가능하지만 왠만하면 지양
  • JPA 객체 그래프 탐색 의도와 맞지 않아 의도치 않은 조작 발생 우려

3. Collection Fetch Join 시 페이징 불가능

데이터 중복이 발생할 수 있기 때문에 페이징 사용 불가능
메모리에서 페이징을 시도할 수 도 있음
Batch Size 컬럼을 사용하면 어느정도 성능 처리를 할 수 있다.
Team : members 필드에 @BatchSize(size = 100) 추가

    @Entity
    public class Team
    {
        @Id @GeneratedValue
        private Long id;
        private String name;
		// @BatchSize(size = 100) 추가 
		@BatchSize(size = 100)
        @OneToMany(mappedBy = "team")
        private List<Member> members = new ArrayList<>();

main : fetch join 을 사용하지 않고 Paging

            String query = "select t from Team t";
            List<Team> result = em.createQuery(query,Team.class)
                    .setFirstResult(0)
                    .setMaxResults(2)
                    .getResultList();
            for (Team t : result) {
                System.out.println("Member : "+t.getMembers() + "| TEAM : "+t.getName());
            }

결과

Hibernate: 
    /* select
        t 
    from
        Team t */ select
            team0_.id as id1_3_,
            team0_.name as name2_3_ 
        from
            Team team0_ limit ?
Hibernate: 
    /* load one-to-many org.example.entity.Team.members */ select
        members0_.TEAM_ID as TEAM_ID8_0_1_,
        members0_.id as id1_0_1_,
        members0_.id as id1_0_0_,
        members0_.city as city2_0_0_,
        members0_.street as street3_0_0_,
        members0_.zipcode as zipcode4_0_0_,
        members0_.age as age5_0_0_,
        members0_.memberType as memberTy6_0_0_,
        members0_.TEAM_ID as TEAM_ID8_0_0_,
        members0_.username as username7_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
Member : [Member{id=3, username='회원1', age=20, address=org.example.entity.Address@57f9b467}, Member{id=4, username='회원2', age=20, address=org.example.entity.Address@6d5c2745}]| TEAM : A
Member : [Member{id=5, username='회원3', age=20, address=org.example.entity.Address@44b29496}]| TEAM : B

@BatchSize(size = 100)를 통해서 LazyLoading 시 한번에 100개 씩 가져온다.
이것을 사용하면 N+1 을 최소화 할 수 있다.

  • 보통 batchsize 를 global 세팅하여 사용 ( value 는 1000 이하로 설정 )
<property name="hibernate.default_batch_fetch_size" value="100" />

정리

대부분의 연관관계는 지연로딩을 사용하고 성능 개선이 필요할 때 fetch Join 을 사용

0개의 댓글