ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • #JPA-21 JPQL - 페치 조인(fetch join)
    JPA 2021. 2. 23. 17:39

    [출처] 인프런 김영한 강사님 -자바 ORM 표준 JPA 프로그래밍 기본

    실무에서 정말정말 중요함

     

    페치 조인(fetch join)

    • SQL 조인 종류X

    • JPQL에서 성능 최적화를 위해 제공하는 기능

    • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능 (쿼리 두번나갈꺼 한방쿼리로 풀때!)

         • join fetch 명령어 사용 • 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로

     

     

    엔티티 페치 조인

    (쿼리를 내가 원하는대로 어던 객체그래프를 한번에 조회할거야하는것을 내가직ㅈ버 명시적으로 동적인 타이밍에 정할수 있는것!)

     

    • 회원을 조회하면서 연관된 팀도 함께 조회(SQL 한 번에)

    • SQL을 보면 회원 뿐만 아니라 팀(T.*)함께 SELECT

    • [JPQL]

       select m from Member m join fetch m.team

           SELECT M이라고 하나만 했는데 M,T DATA 둘다 나열한다. 이게 중요!!!  (즉시 로딩이랑 비슷)

     

    • [SQL]

       SELECT M.*, T.* FROM MEMBER M

       INNER JOIN TEAM T ON M.TEAM_ID=T.ID

     

    Table , query result, 1차 캐시

    Jpa가 entity 5개를 만들어서 1차캐시에 이렇게 보관을 하고, fetch join 그림을 만들어서 한방쿼리로 반환한다.

     

     

    JpaMain

    Team teamA = new Team();
    teamA.setName("팀A");
    em.persist(teamA);
    
    Team teamB = new Team();
    teamB.setName("팀B");
    em.persist(teamB);
    
    Member member1 = new Member();
    member1.setUsername("회원1");
    member1.setTeam(teamA);
    em.persist(member1);
    
    Member member2 = new Member();
    member2.setUsername("회원2");
    member2.setTeam(teamB);
    em.persist(member2);
    
    Member member3 = new Member();
    member3.setUsername("회원3");
    member3.setTeam(teamB);
    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 member : result) {
        System.out.println("member = " + member.getUsername()+ ", "+member.getTeam().getName());
        //여기서 Member 에서의 team은 lazy 지연로딩 때문에 proxy로 설정되어있고
        //, memger.getTeam()을 호출할때마다 DB에서 값을 가져온다.
    }
    tx.commit();
    

    여기서 Member 에서의 team lazy 지연로딩 때문에 proxy로 설정되어있고, memger.getTeam()을 호출할때마다 DB에서 값을 가져온다.

     

    console

    Hibernate: 
        /* select
            m 
        From
            Member m */ select
                member0_.id as id1_0_,
                member0_.age as age2_0_,
                member0_.TEAM_ID as TEAM_ID5_0_,
                member0_.type as type3_0_,
                member0_.username as username4_0_ 
            from
                Member member0_

    여기서 member 3개를 들고옴.

    Hibernate: 
        select
            team0_.id as id1_3_0_,
            team0_.name as name2_3_0_ 
        from
            Team team0_ 
        where
            team0_.id=?
    member = 회원1, 팀A

    Member들은 이미 영속성 컨텍스트에 있고

    iterator로 첫 번째 member를 가지고 올때 getTeam.getName()을 할때 team이 프록시인걸 알고 영속성컨텍스트에 team을 요청하여 DB에서 찾아서 영속성 컨텍스트에 반영한다.

    Hibernate: 
        select
            team0_.id as id1_3_0_,
            team0_.name as name2_3_0_ 
        from
            Team team0_ 
        where
            team0_.id=?
    member = 회원2, 팀B
    member = 회원3, 팀B

    두번째 member을 가지고올때 getTeam.getName()을 하면서 team객체를 DB에서 찾아서 영속성 컨텍스트에 넣고세번째 member를 가지고올떄 getTeam.getName()을 하면 1차 캐시에 이미 두번째 team이랑 같기때문에 DB에 갓다오지 않고 바로 결과가 나온다.

     

    현재 경우에는 쿼리가 3번나갓다 게시판이라고 생각하고 다른 회원 100명이면 쿼리가 100번나간다.. 이런경우를 N +1 (첫번째날린쿼리로 날린 RESULT결과 만큼 N번 도는것)

    이문제를 fetch join으로 해결한다.

     

     

    페치 조인 사용 코드

     console

    Hibernate: 
        /* select
            m 
        From
            Member m 
        join
            fetch m.team */ 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
    member = 회원1, 팀A
    member = 회원2, 팀B
    member = 회원3, 팀B

    - Member를 조인하긴 할건데 일반적 sql join을 하긴하는데 fetch라는 것으로 한번에 끌고와 끌고오는데 뭘끌고 오냐 team을 끌고와.

    - 여기서 member.getTeam의 team은 proxy가 아니다.이미 데이터를 다 조인해서 다들고온상태이다. proxy가아니라 실제 enitty가 담긴것이다.

    - 지연로딩을 세팅해도 패치조인이 세팅이 항상 먼저이다.

     

    username = 회원1, teamname = 팀A

    username = 회원2, teamname = 팀A

    username = 회원3, teamname = 팀B

     

     

    컬렉션 페치 조인

    • 일대다 관계, 컬렉션 페치 조인

    • [JPQL]

    select t
    from Team t join fetch t.members
    where t.name = ‘팀A'

    팀입장에서 meber를 조인하는 것 위에 것이랑 반대 

     

    console

    Hibernate: 
        /* select
            t 
        From
            Team t 
        join
            fetch t.members */ 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
    team = 팀A| members = 1
    team = 팀B| members = 2
    team = 팀B| members = 2

    team = 팀A| members = 1, team = 팀B| members = 2 가 중복되는지 ?

    우리가 원하는 데로 데이터는 가지고오는데 문제가 뭐냐면?

    join이기때문에 데이터가 뻥튀기 될수도 있다. 이건 아래 이미지에서 설명하겠다.

     

    • [SQL]

    SELECT T.*, M.*
    FROM TEAM T
    INNER JOIN MEMBER M ON T.ID=M.TEAM_ID
    WHERE T.NAME = '팀A'

     

     

    DB의 처리결과는 회원이 두명이기때문에 기본적으로 db result는 2번째 표처럼 만든다 그래서 두줄로 만든다.

    teamA입장에선 하난데 Member는 두 줄이된다.

     

    DB에서 두줄가지고오면 그냥 두줄 가져와야한다. DB에서 주면 우선 받아야한다.

    여기서 ID(PK)가 1번이면 영속성컨텍스트에 올려넣고 두번째도 1번이면 같은 영속성 컨텍스트를 쓰는데

    조회한 컬렉션에는 같은 주소값을 가지는게 두명이나온다.

     

     

     

    컬렉션 페치 조인 사용 코드

    console

    Hibernate: 
        /* select
            t 
        From
            Team t 
        join
            fetch t.members */ 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
    team = 팀A| members = 1
    member = jpql.Member@51ec2856
    team = 팀B| members = 2
    member = jpql.Member@33634f04
    member = jpql.Member@4993febc
    team = 팀B| members = 2
    member = jpql.Member@33634f04
    member = jpql.Member@4993febc

    teamname = 팀A, team = Team@0x100

    -> username = 회원1, member = Member@0x200

    -> username = 회원2, member = Member@0x300

     

    teamname = 팀A, team = Team@0x100

    -> username = 회원1, member = Member@0x200

    -> username = 회원2, member = Member@0x300

     

     

    (1:N 은 뻥튀기 , N:1은 뻥튀기 안됨 개수가 맞거나 없으면 줄어듬)

    페치 조인과 DISTINCT 

    • SQL의 DISTINCT는 중복된 결과를 제거하는 명령

    • JPQL의 DISTINCT 2가지 기능 제공

        • 1. SQL에 DISTINCT를 추가

        • 2. 애플리케이션에서 엔티티 중복 제거 (똑같은  entity가 있으면 제거)

     

     

    페치 조인과 DISTINCT 가 안되는 경우

    • select distinct t

       from Team t join fetch t.members

       where t.name = ‘팀A’

     

    • SQL입장에서의 DISTINCT

     SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과 에서 중복제거 실패 (완전히 똑같아야 distinct가 된다)

    console

    Hibernate: 
        /* select
            distinct t 
        From
            Team t 
        join
            fetch t.members */ 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

    쿼리에선  distinct가 되지만쿼리만으로는 distinct가 되지 않는다.

     

     

    페치 조인과 DISTINCT

    DISTINCT가 추가로 애플리케이션에서 중복 제거시도

    같은 식별자를 가진 Team 엔티티 제거

     

     

    [DISTINCT 추가시 결과]

    console

    Hibernate: 
        /* select
            distinct t 
        From
            Team t 
        join
            fetch t.members */ 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
    team = 팀A| members = 1
    member = jpql.Member@1caa9eb6
    team = 팀B| members = 2
    member = jpql.Member@7601bc96
    member = jpql.Member@48a0c8aa

    teamname = 팀A, team = Team@0x100

    -> username = 회원1, member = Member@0x200

    -> username = 회원2, member = Member@0x300

     

     

    페치 조인과 일반 조인의 차이

    • 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음

    • [JPQL]

    select t from Team t join t.members m where t.name = ‘팀A'

    console

    Hibernate: 
        /* select
            t 
        From
            Team t 
        join
            t.members m */ select
                team0_.id as id1_3_,
                team0_.name as name2_3_ 
            from
                Team team0_ 
            inner join
                Member members1_ 
                    on team0_.id=members1_.TEAM_ID
    Hibernate: 
        select
            members0_.TEAM_ID as TEAM_ID5_0_0_,
            members0_.id as id1_0_0_,
            members0_.id as id1_0_1_,
            members0_.age as age2_0_1_,
            members0_.TEAM_ID as TEAM_ID5_0_1_,
            members0_.type as type3_0_1_,
            members0_.username as username4_0_1_ 
        from
            Member members0_ 
        where
            members0_.TEAM_ID=?
    team = 팀A| members = 1
    member = jpql.Member@53c6f96d
    Hibernate: 
        select
            members0_.TEAM_ID as TEAM_ID5_0_0_,
            members0_.id as id1_0_0_,
            members0_.id as id1_0_1_,
            members0_.age as age2_0_1_,
            members0_.TEAM_ID as TEAM_ID5_0_1_,
            members0_.type as type3_0_1_,
            members0_.username as username4_0_1_ 
        from
            Member members0_ 
        where
            members0_.TEAM_ID=?
    team = 팀B| members = 2
    member = jpql.Member@3c98781a
    member = jpql.Member@3f736a16
    team = 팀B| members = 2
    member = jpql.Member@3c98781a
    member = jpql.Member@3f736a16

    컬렉션은 프록시는 아닌데 데이터가 로딩시점에 없기때문에 쿼리가 쭉쭉 나감

    -> 조인 패치로 배꾸게 되면 쿼리한번에 나가고 select절에 데이터 다포함되있고 지연로딩이 없다.

     

     

    • [SQL]

    SELECT T.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'

     

    페치 조인과 일반 조인의 차이

    • JPQL은 결과를 반환할 때 연관관계 고려X

    • 단지 SELECT 절에 지정한 엔티티만 조회할 뿐

    • 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회X

     

    • 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)

    페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

     

     

    페치 조인 실행 예시

    • 페치 조인은 연관된 엔티티를 함께 조회함

    • [JPQL]

    select t from Team t join fetch t.members where t.name = ‘팀A'

    • [SQL]

    SELECT T.*, M.* FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'

     

    페치 조인의 특징과 한계

    페치 조인 대상에는 별칭을 줄 수 없다.

        • 하이버네이트는 가능, 가급적 사용X

      - 패치조인은 나랑연관된 것을 다끌고 오겠다는 건데, 성능상 중간에 몇개를 걸러오고싶다고 해도 안된다.

          - 걸러오고싶으면 따로 조회해야한다.

       - 팀과 연관된 애들이 5명이라고 치자 그중에 한명만 불러올려고하는데 잘못조작하게되면 나머지 4명이 누락되있어  서 이상하게 동작할 수 있다.

        - 이렇게 때문에 별칭 주지 않는게 관례이다. 

        - 팀을조회하는데 맴버가 5명이있는데 그중에 3명만 조회했어 3명만 따로 조작한다는거 자체가 위험하다.

    왜냐하면 jpa에서 객체그래프를 . 으로 찍어서 가면 팀에서 멤버로간다 원래 5명이 나와야하는데 where m.age>10 로 3개만 뽑혓다.

    team.mebers로가면 3개명 밖에 안나오는거다 app입장에선 jpa에서 의도한 설계는 이렇게 부부능로 조회하는 것이 아니고 team.members할때 맴버 한태 다갈수 있어야한다.

    객체 그래프라는 것은 기본적으로 데이터를 다 조회해야하는거다.

    거르면서 조회하는게 좋을거같지만 이건 team에서 members를 가져오면 안되고  처음부터 맴버를 5개 조회해야한다. 연관관계를 찾아간다는 것은  team.members를 하면 members를 다 찾아간다고 가정하고 설계되어잇다.

    이상한옵션에 다발라져 있으면 나머지가 이상하게 나올 수 도있다. 결론 alias는 웬만해서 쓰면안된다.

     

       - 사용하는 경우 -join fetch를 몇단계로 가져갈떄 

    둘 이상의 컬렉션은 페치 조인 할 수 없다.

       - 1:N:N 라서 데이터가 예상하지 못하게 늘어나면서 ** 가 될수 있다. 데이터가 잘안맞다.

       - 패치조인 컬렉션은 딱하나만 슬 수 있다.

    컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.

        • 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능

           - 일대다는 데이터 뻥튀기가 된다 이걸로 페이징을 한다? 왜안되냐면?

               - TEAMA가 DATA2건이라치고 이걸 패치조인하면 2건나온다. PAGE사이즈 1로 줫다.

                 그럼 반으로 잘리고 하나만 가져온다 그럼 TEAMA는 회원 1만 가지고 있다라고 생각하기 때문에 안된다.

               - PAGE는 DB중심적이고 ROW수를 어떻게 줄일지를 생각하는건데 그런데 컬렉션을 패치조인하면

                JPA에서는 TeamA 하나고 얘는 회원 1밖에 없어 라고 결과가 나온다.

     

        • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험) 절대 쓰면안된다.

          JpaMain Class

     String query = "select t From Team t join fetch t.members as m";
                List<Team> result = em.createQuery(query, Team.class)
                        .setFirstResult(0)
                        .setMaxResults(1)
                        .getResultList();

          console

    Hibernate: 
        /* select
            t 
        From
            Team t 
        join
            fetch t.members as m */ 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

           - firstResult/maxResults specified with collection fetch; applying in memory! 라는 에러가 나온다.

     

    그럼 컬렌셕을 패치조인 할수있는 방법은 없을까?

    -1. 지금 캐이스에서는 일대다 다대일이면 반대로 할 수 있기때문에 

    쿼리를 바꾸는것이다. 

    회원에서 팀으로가는것은 다대일이기 떄문에 문제가 없다.

     

    -2. 패치조인을 사용하지 않는것.  

    JpaMain

    console.

    Hibernate: 
        /* select
            t 
        From
            Team t */ select
                team0_.id as id1_3_,
                team0_.name as name2_3_ 
            from
                Team team0_ limit ?
    Hibernate: 
        select
            members0_.TEAM_ID as TEAM_ID5_0_0_,
            members0_.id as id1_0_0_,
            members0_.id as id1_0_1_,
            members0_.age as age2_0_1_,
            members0_.TEAM_ID as TEAM_ID5_0_1_,
            members0_.type as type3_0_1_,
            members0_.username as username4_0_1_ 
        from
            Member members0_ 
        where
            members0_.TEAM_ID=?
    team = 팀A| members = 1
    member = jpql.Member@53d13cd4
    Hibernate: 
        select
            members0_.TEAM_ID as TEAM_ID5_0_0_,
            members0_.id as id1_0_0_,
            members0_.id as id1_0_1_,
            members0_.age as age2_0_1_,
            members0_.TEAM_ID as TEAM_ID5_0_1_,
            members0_.type as type3_0_1_,
            members0_.username as username4_0_1_ 
        from
            Member members0_ 
        where
            members0_.TEAM_ID=?
    team = 팀B| members = 2
    member = jpql.Member@429f7919
    member = jpql.Member@4a2929a4

     되긴되지만 쿼리가 3번이나 돌았다 팀이 10개면 추가로 더늘어남 성능상 좋지않다.

     

    -3. batchsize

    teamId 가?,?  두개가 들어가있다 . 한번에 teamA에 연관된 맴버와 teamB연관된 맴버를 다들고 오는것이다.

    이옵션이 무슨옵션인가 하면은 team을 가져올때 현자 member는 Lazy loading 상태이다  그런데 lazy loading을 딱

    끌고올대 내팀뿐만아니라 list에 담긴 팀을 한번에 in query로 100개씩 넘긴다 지금은 2개바께없으니깐 2개만 넘긴다.

     

    이걸이용하면 table이 team을 패이징 쿼리하는데 10개가있는데 team과 연관된 걸 찾는 쿼리가 10번이 나가야하는데

    그래서 팀조회 1번 맴버 조회 10번 N+1 문제  (N 팀을가져온거에 결과 갯수) 를 해결할 수 있다.(Fetch 조인으로도 해결할  수있다.      

     

    • 연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화

    • 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함

        • @OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략

    • 실무에서 글로벌 로딩 전략은 모두 지연 로딩

    • 최적화가 필요한 곳은 페치 조인 적용

     

     

    페치 조인 - 정리

    • 모든 것을 페치 조인으로 해결할 수 는 없음

    • 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적 

    • 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 하면,

      페치 조인 보다는 일반 조인을 사용하고 필요 한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

      -3가지 방법

         1. entity를 패치조인을써서 entity를 조회해온다.

         2. 패치조인을 열심히해서 application 에서 dto로 바꿔서 화면에 ㅂ=ㅏㄴ화

         3. 처음부터 new operation으로 스위칭해서 가져온다.

     

     

    댓글

Designed by Tistory.