Web/JPA

EP13. JPQL 경로표현식, 페치조인, 다형성, named, 벌크연산

경로표현식 구분

 

점을 찍어 객체 그래프를 탐색하는 것

 

상태필드

  • 단순히 값을 저장하기 위한 필드
  • 경로 탐색의 끝. 더이상 탐색 X

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

단일 값 연관 경로

  • 묵시적 내부 조인 발생 (묵시적 내부조인은 사용하지 않는 것이 좋다)
  • 탐색 더 가능

 

컬렉션 값 연관 경로 (1 대 다 관계)

  • 묵시적 내부 조인 발생
  • 탐색 X (from절에서 명시적 조인을 사용해서 별칭을 얻어서 사용하면 탐색이 가능하다)

 

결론 : 실무에서는 묵시적조인을 사용하지 않고 명시적 조인만 사용한다.

 

fetch join (페치조인)

  • JPQL에서 성능 최적화를 위해 제공하는 기능
  • 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
  • join fetch 로 사용

 

사용예제

회원 1 : teamA

회원 2 : teamA

회원 3 : teamB

상황일때

 

 

페치조인 사용X

String query = "select m from Member m";

List<Member> resultList = em.createQuery(query, Member.class)
        .getResultList();

for (Member member : resultList) {
    System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}

실행결과

Member와 Team은 일대다 관계이고 지연로딩설정을 해놓았기 때문에 member.getTeam() 시점에서 team 쿼리가 날아간다.

* SQL 3개

Member List : SQL

teamA : SQL

teamA : 영속성 컨텍스트

teamB : SQL

두번째 teamA는 이미 SQL가 날아가고 영속성 컨텍스트에 올라갔기 때문에 SQL이 나가지 않는다.

 

패치조인 사용O

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

List<Member> resultList = em.createQuery(query, Member.class)
        .getResultList();

for (Member member : resultList) {
    System.out.println("member = " + member.getUsername() + ", " + member.getTeam().getName());
}

페치조인으로 회원과 팀을 함께 조회해서 지연로딩없이 미리 가져온다.

 

 

실행결과

 

 

컬렉션 페치 조인 (일 대 다) 도 가능하다.

그런데 team과 member를 조인할 때 팀과 연결된 멤버 수 대로 테이블이 구성된다.

  1. 팀A는 처음에 쿼리가 나가고 영속성 컨텍스트에 등록된다. 그래서 2번째 팀A는 영속성 컨텍스트에서 가져와 사용하기 때문에 1개로 표현한다.
  2. 회원은 컬렉션 fetch join (일대다) 일때 DB입장에서는 회원의 개수만큼 가져와서 2개가 된다.

예제코드

            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() + " | " + team.getMembers().size());
                for (Member member : team.getMembers()) {
                    System.out.println("-> member = " + member);
                }
            }

결과

 

 

페치조인, DISTINCT

 

  • sql의 distinct : 완전히 일치하면 중복 제거
  • jpql의 distinct : 완전히 일치하면 중복제거, 같은 식별자를 가진 엔티티 중복 제거

 

String query = "select distinct t from Team t join fetch t.members";

2번째 team이 영속성 컨텍스트에 등록된 엔티티를 가져오기 때문에 같은 엔티티이고 중복이 제거된다.

 

 

 

페치조인 vs 일반조인

 

페치조인

String query = "select t from Team t join fetch t.members";

fetch join 이므로 t를 가져올때 member까지 미리 가저온다.

 

 

일반조인

String query = "select t from Team t join t.members m";

select절에 t만 있으므로 m은 당연히 가져오지 않는다. 그래서 나중에 team에서 getMembers를 할때 쿼리문이 날아간다.

 

 

 

페치 조인의 특징과 한계

  1. 페치 조인 대상에는 별칭을 줄 수 없다.
    (별칭을 준다는 것은 where로 조건을 줄 수 있다는 것인데, jpa는 연관된 객체그래프를 전부 가져온다고 생각하고 다른 옵션(cascade 등)을 부여할 수 있기 때문에 애초에 별칭을 주지 않고 사용하자는 관례가 있다.)
  2. 둘 이상의 컬렉션은 페치 조인 할 수 없다.
  3. 컬렉션을 페치 조인하면 페이징 API(setFirstREsult, setMaxResults)를 사용할 수 없다.
    (join fetch를 뺴고 페이징 API를 사용하고 컬렉션을 가지고있는 엔티티에는 batch size를 줘서 한번에 가져오는 엔티티 수를 정할 수 있다.)

 

 

정리

  1. 모든 글로벌 로딩 전략은 지연로딩으로 한다.
  2. 필요한 경우 페치조인으로 가져온다.

 

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

  • 엔티티를 페치조인으로 조회해 온다. 애플리케이션에서 DTO로 바꿔서 화면에 반환한다.
  • 처음 jpql을 짤때부터 new operation으로 스위칭해서 가져온다

 

 

 

 

 

다형성 쿼리

Item 중에서 Book, Movie만 가져오고 싶을 때

 

  • JPQL
    select i from Item i where type(i) IN (Book, Moive)
  • SQL
    select i from item i where i.DTYPE IN ('B', 'M')

Treat

  • JPQL
    select i from Item i where treat(i as Book).author = 'kim'
  • SQL
    select i from item i where i.DTYPE = 'B' and i.author = 'kim'

 

 

 

JPQL 엔티티 직접사용

  • JPQL
    select count(m.id) from Member m // 엔티티의 아이디를 사용
    select count(m) from Member m // 엔티티를 직접 사용
  • SQL
    select count(m.id) as cnt from Member m // 위 두 JPQL 둘다 이 SQL 실행

JPQL에서 엔티티를 직접사용하면 '기본키'를 사용한다.

+ DB에서 외래키로 되어있는 엔티티도 엔티티 그대로 넘기면 외래키를 넘긴것과 같다.

 

 

 

 

Named 쿼리 - 어노테이션

  • 미리 정의해서 이름을 부여해두고 사용하는 JPQL
  • 정적쿼리만 가능
  • 어노테이션, XML에 정의
  • 애플리케이션 로딩시점에 초기화 후 재사용
  • 애플리케이션 로딩 시점에 쿼리를 검증

Spring Data JPA 에서 jpa가 사용한다.

 

 

 

 

벌크 연산

재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?

너무 많은 update query가 실행된다.

 

JPQL은 벌크연산이 가능하다.

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);

쿼리 한번으로 여러 테이블 로우 변경 가능

 

 

벌크 연산 주의

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

 

벌크연산 코드

            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.setAge(0);
            member1.setTeam(teamA);
            em.persist(member1);

            Member member2 = new Member();
            member2.setUsername("관리자2");
            member2.setAge(0);
            member2.setTeam(teamA);
            em.persist(member2);

            Member member3 = new Member();
            member3.setUsername("관리자3");
            member3.setAge(0);
            member3.setTeam(teamB);
            em.persist(member3);

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

            int resultCount = em.createQuery("update Member m set m.age = 20")
                    .executeUpdate();

            System.out.println("resultCount = " + resultCount);

            System.out.println("member1.getAge() = " + member1.getAge());
            System.out.println("member2.getAge() = " + member2.getAge());
            System.out.println("member3.getAge() = " + member3.getAge());

출력결과

영속성 컨텍스트를 무시하기 때문에 member의 나이를 20살로 업데이트해도 영속성 컨텍스트에서 age를 가져오면 0살로 되어있다.

 

그래서, 아래 두가지 방법중 하나를 선택해서 사용해야한다.

  1. 벌크 연산만 먼저 실행
  2. 벌크연산 수행 후 영속성 컨텍스트 초기화

대부분 2번을 적용해준다 ( 벌크연산 후 em.clear() )

 

'Web > JPA' 카테고리의 다른 글

EP2. 지연 로딩과 조회 성능 최적화  (0) 2021.04.06
EP1. 회원관리 API 만들기  (0) 2021.04.05
EP12. JPQL (SQL식 JPQL변환)  (0) 2021.02.21
EP11. JPQL소개, 기본문법  (0) 2021.02.19
CascadeType.REMOVE vs orphanRemoval = true  (0) 2021.02.13