WEB/JPA

김영한 (ORM 표준 JPA 프로그래밍 11) 객체지향 쿼리 언어 소개2 - 중급문법

Tony Lim 2021. 3. 20. 23:26
728x90

 

경로 표현식 용어 정리

상태필드 = 단순히 값을 저장하기위한 필드 예) m.username

경로탐색의 끝, 더 이상 탐색하지않는다.

 

명시적 조인 = join 키워드 직접사용

 

select m from Member m join m.team t

묵시적 조인 = 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)

select m.team from Member m

 

연관 필드 = 연관관계를 위한필드

단일 값 연관 필드
@ManyToOne , @OneToOne 대상이 엔티티다 예) m.team

            String query = "select m.team From Member m";
            List<Team> resultList = em.createQuery(query, Team.class)
                    .getResultList();
Hibernate: 
    /* select
        m.team 
    From
        Member m */ select
            team1_.id as id1_3_,
            team1_.name as name2_3_ 
        from
            Member member0_ 
        inner join
            Team team1_ 
                on member0_.TEAM_ID=team1_.id

묵시적 내부조인(inner join) 발생, 탐색이 더 가능하다

 

컬렉션 값 연관 필드
@OneToMany, @ManyToMany, 대상이 컬렉션 예) m.orders , t.members 에서는 size 만 가능하다. 더 이상 객체 그래프를 탐색을 할 수 없다. -> 명시적 조인을 통해 별칭을 얻으면 가능하다.

m.username의 경우 상태필드에 도달했기에 더이상 경로탐색이 당연하게 불가능하다.

select t.members.(username 안됨) From Team t;
select m.username From Team t join t.members m; // 명시적 조인을 통해 별칭을 얻으면 가능

 

select o.member.team from Order o; // 묵시적 조인
select t from Order o
join o.member m
join m.team t  // 명시적 조인

항상 뭘 하든 명시적 조인을 써야 한다!!! 묵시적 조인은 위험한 존재이다.

 

JPQL fetch join - 1

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

 

회원을 조회하면서 연관된 팀도 함께 조회하고 싶다

JPQL

select m from Member m join fetch m.team

SQL

select M.* , T.* from Member M
inner join Team T on M.team_id = T.id

sql 을 보면 회원 뿐만 아니라 팀 T.* 함께 select 된다. 즉시로딩이라 같은 것이다.

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

            for (Member member : resultList)
            {
                System.out.println("member.getUsername() = " + member.getUsername() + ", " + member.getTeam().getName());
            }
Hibernate: 
    /* select
        m 
    From
        Member m */ select
            member0_.id as id1_0_,
            member0_.age as age2_0_,
            member0_.TEAM_ID as team_id4_0_,
            member0_.username as username3_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.getUsername() = member1, TeamA
member.getUsername() = member2, TeamA
Hibernate: 
    select
        team0_.id as id1_3_0_,
        team0_.name as name2_3_0_ 
    from
        Team team0_ 
    where
        team0_.id=?
member.getUsername() = member3, TeamB

 

쿼리가 첫 for 문에서 getTeam().getName()을 실행할때 team을 select 해온다 지연로딩이기 때문이다. lazy이기에 현재는 team 에 프록시 객체가 들어가 있기 때문이다.

그 다음에 2번쨰 for문에서 첫번째 for문에서 회원1을 불러올떄 TeamA를 1차 캐시(영속성 context)에 저장을 해두었기 때문에 그냥 1차 캐시에서 가져온다. 

마지막에는 회원 3은 TeamB 이기 때문에 다시 쿼리를 날려서 가져온다.

이런 현상을 1+N 즉 Member의 Team을 알아보기 위해 날린 1개의 쿼리 때문에 추가적으로 N 개의 쿼리가 호출 되는것이다.

미리 fetch join을 써서 한번에 가져와 이런일을 방지하는것이 좋다. 또한 지연로딩을 선택했더라도 fetch join이 항상 우선순위를 가진다.

 

 

컬렉션 패치 조인 = 일대다 관계, 컬렉션 패치 조인

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"
            String query = "select t From Team t join fetch t.members";
            List<Team> resultList = em.createQuery(query, Team.class)
                    .getResultList();
            for (Team team : resultList)
            {
                System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
                for (Member member : team.getMembers())
                {
                    System.out.println("-> member = " + member);
                }
            }
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_id4_0_1_,
            members1_.username as username3_0_1_,
            members1_.TEAM_ID as team_id4_0_0__,
            members1_.id as id1_0_0__ 
        from
            Team team0_ 
        inner join
            Member members1_ 
                on team0_.id=members1_.TEAM_ID
team = TeamA|members=2
-> member = jpql.Member@55651434
-> member = jpql.Member@60a19573
team = TeamA|members=2
-> member = jpql.Member@55651434
-> member = jpql.Member@60a19573
team = TeamB|members=1
-> member = jpql.Member@134ff8f8

위에 저런식으로 팀A에 있는 Member가 2명이기 때문에 row가 하나 더 생겨서 총 3개의 Team이 있는것처럼 불러오는 것이다.

이런 중복을 제거하기 위해서는 sql 의 distinct로는 불가능하다. row 자체가 완전히 똑같아야지만 중복으로 인식하기 때문이다.

JPA 의 distinct 는 추가로 애플리케이션에서 중복을 제거 해준다. 같은 식별자를 가진 Team 엔티티는 제거해준다.

            String query = "select distinct t From Team t join fetch t.members";
team = TeamA|members=2
-> member = jpql.Member@3c91530d
-> member = jpql.Member@1115433e
team = TeamB|members=1
-> member = jpql.Member@513b52af

중복이 제거된것을 확인 할 수 있다.

 

패치 초인과 일반 조인의 차이

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다.

JPQL(그냥조인)은 결과를 반환할 때 연관관계를 고려하지않고 select 절에 지정한 엔티티만 조회 한다. 

패치조인은 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩) , 객체 그래프를 SQL한번에 조회하는 개념

 

즉시로딩과 fetch 조인의 차이

https://www.inflearn.com/questions/39516

https://velog.io/@jinyoungchoi95/JPA-%EB%AA%A8%EB%93%A0-N1-%EB%B0%9C%EC%83%9D-%EC%BC%80%EC%9D%B4%EC%8A%A4%EA%B3%BC-%ED%95%B4%EA%B2%B0%EC%B1%85

em.find 일시에는 fetch join 마냥 동작을 한다. 하지만 jpql 은 이야기가 다르다.

즉시로딩으로 설정이 되어있어도 select m from member m 을 하게되면

jpql 은 eager와 무관하게 sql 로 그대로 번역 -> select m.* from member

member만 조회하고 team은 조회하지 않음 -> 나중에 jpa 가 eager임을 발견하고 추가적으로 team 을 가져오는 쿼리를 날림 -> N+1 문제 발생

 

 

패치 조인의 특징과 한계

패치 초인 대상에는 별칭을 줄 수 없다. (하이버네이트는 가능 하지만 사용지양)

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

컬렉션을 패치 조인하면 페이징 API(setFirstResult , setMaxResults) 를 사용할 수 없다. 데이터 뻥튀기때문.

  • 일대일 , 다대일 같은 단일 값 연관 필드들은 패치 조인해도 페이징 가능
  • 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우위험)

방향을 뒤집어서 데이터 뻥튀기가 안일어나게 해서 페이징하면 문제가 없다.

    @BatchSize(size = 100)
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();
            String query = "select t From Team t";
            List<Team> resultList = em.createQuery(query, Team.class)
                    .setFirstResult(0)
                    .setMaxResults(2)
                    .getResultList();
            for (Team team : resultList)
            {
                System.out.println("team = " + team.getName() + "|members=" + team.getMembers().size());
                for (Member member : team.getMembers())
                {
                    System.out.println("-> member = " + member);
                }
            }
Hibernate: 
    /* load one-to-many jpql.Team.members */ select
        members0_.TEAM_ID as team_id4_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_id4_0_0_,
        members0_.username as username3_0_0_ 
    from
        Member members0_ 
    where
        members0_.TEAM_ID in (
            ?, ?
        )
team = TeamA|members=2
-> member = jpql.Member@47ac613b
-> member = jpql.Member@6bccd036
team = TeamB|members=1
-> member = jpql.Member@4f4c88f9

 

@Batchsize(size=100) 를 통하여 페이징을 해결할 수 있다. 

글로벌 세팅도 가능하다.  hibernate.default_batch_size value = "100" .

150개의 팀이 존재한다고 했을때 1~100 팀에 해당되는 member.Team_id in ( 1~100) 이라는 쿼리를 날려서 연관된 엔티티모두를 select해서 받아온다.  그리고 나머지 member.Team_id in (101~150) 이라는 쿼리를 날려서 받아온다.

 

엔티티 직접 사용 - 기본 키 값

JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용

JPQL

select count(m.id) from Member m // 엔티티의 PK를 사용
select count(m) from Member m // 엔티티를 직접사용

SQL

select count(m.id) as cnt from Member m

 

@NamedQuery

위와 같이 그냥 쿼리를 함수처럼 재사용하게 해주는 기능이다.

jpa 가 미리 jpql 로 작성된것을 파싱하고 캐싱하고있어서 productive 하다.

애플리케이션 로딩 시점에서 쿼리를 검증가능 = 실행하는 시점에 String인데 불구하고 오타 및 오류를 잡아준다. 나중에 Spring Data JPA를 배우면 이것을 한번 더 맛 볼 것이다.

 

벌크 연산

쿼리 한 번으로 여러 테이블 로우 변경(엔티티)

executeUpdate()의 결과는 영향 받은 엔티티 수 반환

Update, Delete 지원 

 

벌크연산 주의

벌크연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다. 2가지 방법이 존재한다.

1. 벌크 연산을 먼저 실행 

2. 벌크 연산 후 영속성 컨텍스트 초기화

em.clear를 안하는 경우에 영속성 컨텍스트가 옛날것이 그대로 남아 있어서 데이터 정합성 문제가 생긴다.

728x90