JPQL 중급
경로 표현식
점(.)을 찍어 객체 그래프를 탐색하는 것
종류
상태 필드(state field)
- 단순히 값을 저장하기 위한 필드
- 경로 탐색의 끝이므로 더 이상 탐색 불가능
연관 필드(association field)
- 연관관계를 위한 필드
- 단일 값 연관 필드
- @XtoOne, 대상이 엔티티
- 묵시적 내부 조인(inner join), 탐색 가능
- 컬렉션 값 연관 필드
- @OneToX, 대상이 컬렉션
- 묵시적 내부 조인X, 탐색 불가능
- From 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색 가능
묵시적 내부 조인이 발생하면 쿼리 튜닝이 어렵고 운영하는데 불편하기 때문에 지양할 것
조인
명시적 조인
JOIN 키워드 직접 사용
select m from Member m join m.team t
묵시적 조인
경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 발생)
select m.team from Member m
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야 탐색 가능
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만 묵시적 조인으로 인해 SQL의 FROM(JOIN) 절에 영향을 줌
실무에서는!
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝의 중요 포인트 - ORM이 객체지향적이어도 기승전 DB이기 때문에 튜닝은 필수..
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
Fetch Join
- SQL 조인의 종류가 아니라 JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- (LEFT/OUTER/INNER) JOIN FETCH
일반 조회
select m from Member m
1. 모든 멤버 조회
- 팀A 조회
- 팀B 조회
Member만 조회하고 싶은데(1) Team을 조회하는 쿼리까지 발생(N) => N + 1 문제
페치 조인을 통한 N+1 해결
ex) 회원을 조회하면서 연관된 팀도 함께 조회 (SQL 1번)
select m from Member m join fetch m.team
컬렉션 페치 조인
ex) 각 팀 별 회원 조회
select t From Team t join fetch t.members
- 팀은 1개인데 소속 회원이 2명이라서 팀A에 대한 데이터는 2개 생성
DISTINCT
- SQL의 DISTINCT는 중복된 결과를 제거하는 명령어
- JPQL의 DISTINCT는 2가지 기능 제공
1. SQL에 DISTINCT 추가
- 애플리케이션에서 엔티티 중복 제거
- 대부분 다대일 보단 일대다에서 사용
하지만, SQL에 DISTINCT를 추가한다해도 데이터가 완벽하게 같지 않으므로 중복제거 실패
이를 해결하기 위해 JPA에서 DISTINCT가 추가로 애플리케이션에서 중복 제거시도, 같은 식별자를 가진 Team 엔티티 제거
패치 조인과 일반 조인의 차이
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않고, 단지 SELECT 절에 지정한 엔티티만 조회한다.
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념
특징
성능 최적화
연관된 엔티티들을 SQL 한 번으로 조회
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
실무에서 글로벌 로딩 전략은 모두 지연 로딩
최적화가 필요한 곳(N+1)은 페치 조인 적용
한계
페치 조인 대상에는 별칭을 줄 수 없다.
- 하이버네이트는 가능하지만 가급적이면 사용X
- 팀과 연관된 회원 5명이라고 할 때, 부분적으로 1명 혹은 3명을 따로 가져와서 조작한다는 것은 위험하다.
- 또한, Members에는 5명 중 3명만 담겨있기 때문에 나머지 2명에게는 그래프 탐색을 하지 못한다. 이는 모든 객체를 탐색할 수 있다는 JPA의 의도와 어긋난다.
둘 이상의 컬렉션은 페치 조인 할 수 없다.
최악의 경우 일대다대다 관계가 될 수 있어서 데이터가 예상하지 못하게 중복되는 상황이 발생할 수 있다.
컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징 가능
- 하이버네이트는 경고 로그를 남기고 메모리에서 페이징(매우 위험)
- 쿼리 결과 데이터를 모두 메모리에 저장함 (만약 데이터가 100만건이라면?)
@BatchSize 를 통해 페이징 구현 가능
정리
모든 것을 페치 조인으로 해결할 수는 없다.
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
다형성 쿼리
TYPE
select i from Item i where type(i) IN (Book, Movie)
- 조회 대상을 특정 자식으로 한정
- SQL에서는 @DiscriminatorColumn에 명시한 이름으로 쿼리 발생
TREAT
select i from Item i where treat(i as Book).author = 'kim'
- 자바의 타입 캐스팅과 유사
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
- FROM, WHERE, SELECT에서 사용
엔티티 직접 사용
기본 키 값
- JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 값을 사용
- JPQL에서 엔티티 -> SQL에서 엔티티 id
외래 키 값
- 기본 키와 마찬가지로 연관관계 엔티티 -> SQL에서 연관관계 엔티티 id(외래 키)
Named 쿼리
정적 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL
- 어노테이션, XML에 정의
- 애플리케이션 로딩 시점에 초기화 후 재사용
- 애플리케이션 로딩 시점에 쿼리를 검증 (컴파일 오류로 즉각적인 해결 가능)
어노테이션
@NamedQuery(name = "", query = "")
XML
- 항상 우선권을 가짐
- 애플리케이션 운영 환경에 따라 다른 XML 배포 가능
persistence.xml
<mapping-file>META-INF/ormMember.xml</mapping-file>
ormMember.xml
<named-query name="">
<query></query>
</named-query>
벌크 연산
재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행한다.
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행 -> 쿼리 한 번으로 상품의 정보를 업데이트하는 벌크 연산 기능 제공
사용
em.createQuery("update").executeUpdate();
- 쿼리 한 번으로 여러 테이블 로우(엔티티) 변경
- executeUpdate()의 결과는 영향받은 엔티티 수(int) 변환
- UPDATE, DELETE, INSERT(하이버네이트 지원)
- 연산 실행시 flush 자동 호출
주의
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리 실행
- 벌크 연산 실행 후 영속성 컨텍스트의 값은 그대로 있음
벌크 연산 먼저 실행 -> 영속성 컨텍스트 초기화
- em.clear() 후 em.find로 다시 DB에서 조회하면 정상적인 결과 반환