[김영한 - JPA 기본] #10 JPQL - 2
JPQL 경로 표현식
- .(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -- 상태 필드
from Member m
join m.team t -- 단일 값 연관 필드
join m.orders o -- 컬렉션 값 연관 필드
where t.name = '팀A'
경로 표현식 용어 정리
- 상태 필드(state field): 단순히 값을 저장하기 위한 필드
- (ex: m.username)
- 연관 필드(association field): 연관관계를 위한 필드
- 단일 값 연관 필드:
@ManyToOne, @OneToOne, 대상이 엔티티 (ex: m.team) - 컬렉션 값 연관 필드:
@OneToMany, @ManyToMany, 대상이 컬렉션 (ex: m.orders)
- 단일 값 연관 필드:
경로 표현식 특징
- 상태 필드(state field): 경로 탐색의 끝, 탐색이 더이상 불가하다
- 단일 값 연관 경로:
묵시적
내부 조인(inner join
) 발생, 탐색이 더 가능하다 - 컬렉션 값 연관 경로:
묵시적
내부 조인 발생, 탐색이 더이상 불가- FROM 절에서 명시적 조인을 통해 별칭(
alias
)을 얻으면 별칭을 통해 탐색 가능
- FROM 절에서 명시적 조인을 통해 별칭(
상태 필드 경로 탐색
- JPQL
select m.username, m.age from Member m
- SQL
select m.username, m.age from Member m
단일 값 연관 경로 탐색
- JPQL
select o.member from Order o
- SQL
select m.*
from Orders o
inner join Member m on o.member_id = m.id
명시직 조인, 묵시적 조인
- 명시적 조인:
join
키워드 직접 사용
select m from Member m join m.team t
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생 (내부 조인만 가능)
select m.team from Member m
경로 표현식 예제
select o.member.team from Order o -- 가능
select t.members from Team -- 가능
select t.members.username from Team t -- 불가
select m.username from Team t join t.members m -- 가능
경로 탐색을 사용한 묵시적 조인 시 주의사항
- 항상 내부 조인
- 컬렉션은 경로 탐색의 끝, 명시적 조인을 통해 별칭을 얻어야함
- 경로 탐색은 주로 SELECT, WHERE 절에서 사용하지만
묵시적 조인으로 인해 SQL의 FROM (JOIN) 절에 영향을 줌
실무 조언
- 가급적 묵시적 조인 대신에 명시적 조인 사용
- 조인은 SQL 튜닝에 중요 포인트이기 때문에 반드시 직관적으로 잘 나타내자
- 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
JPQL 페치 조인 (fetch join)
실무에서 정말정말 중요한 !
- 일반적인 DB SQL 조인이 아님!
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
[ LEFT [OUTER] | INNER ] JOIN FETCH
조인 경로
엔티티 페치 조인
- 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
- 회원을 조회하면서 연관된 팀(T.*)도 함께 조회 (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'
문제는 이 경우 모든 가능한 경우가 발생하여 팀 객체가 여러개 보이는 것처럼 선택된다 !
(강의 pdf 그림을 참고하면 바로 알 수 있다)
페치 조인과 DISTINCT
SQL의
DISTINCT
는 중복된 결과를 제거하는 명령이었다JPQL의
DISTINCT
는 2가지 기능을 제공- SQL에
DISTINCT
를 추가
- SQL에
- 애플리케이션에 엔티티 중복 제거
- 위 JPA find의 결과로 나온 중복된 팀을 제거할 수 있다
- 애플리케이션에 엔티티 중복 제거
페치 조인과 일반 조인의 차이
- 일반 조인 실행시 연관된 엔티티를 함께 조회하지 않는다
- 페치 조인은 연관 엔티티를 함께 조회, 즉 즉시 로딩한다
select t
from Team t join t.members m
where t.name = ‘팀A'
분명 조인을 헀음에도 불구하고 .. 연관 엔티티는 무시된다
아래 처럼 !!
SELECT T.*
FROM TEAM T
INNER JOIN MEMBER M
...
페치조인의 특징과 한계
페치 조인 대상에는 별칭을 줄 수 없다
- 하이버네이트는 가능, 가급적 사용 (X)
둘 이상의 컬렉션은 페치 조인하면 안됀다 !
- 컬렉션을 페치 조인하면 페이징 API (setFirstResult, setMaxResults)를 사용할 수 없다
- 앞에서 본 것 처럼 여러 Row가 뻥튀기 된다 ㅠㅠ
@XXX ToOne
시리즈는 가능 하다 (연관 엔티티가 컬렉션이 아닌 단일 값의 경우)- 뻥튀기 되는 현상이 없기 때문
- 하이버 네이트는 단지 경고 로그만 남길 뿐, 메모리에서 페이징 처리를 한다
- 컬렉션을 페치 조인하면 페이징 API (setFirstResult, setMaxResults)를 사용할 수 없다
페치 조인을 사용하는 것이 엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선한다
- 아래와 같은 경우를 글로벌 로딩 전략이라고 한다, 전역적으로 설정이 할당되기 때문
- 생각해보면 당연하다, repository, service는 도메인 객체를 의존하기 때문에
도메인 객체 자체를 전역 변수같은 느낌으로 받아들이면글로벌 로딩
이라는 네이밍이 이해가 간다
- 생각해보면 당연하다, repository, service는 도메인 객체를 의존하기 때문에
@OneToMany(fetch = FetchType.LAZY)
- 아래와 같은 경우를 글로벌 로딩 전략이라고 한다, 전역적으로 설정이 할당되기 때문
실무에서 글로벌 로딩 전략은 모두 지연 로딩
- 이 중 최적화가 필요한 곳만 페치 조인을 적용한다
@BatchSize
- Team에서 Member 객체를 조회할 때 (1+N 문제)
- fetch 조인이 아닌 이 옵션으로 해결 가능하다
@Entity
public class Team { ...
@BatchSize(size = 10)
private List<Member> members = new ArrayList<>();
/* load one-to-many jpql.Team.members */ select
members0_.TEAM_ID as TEAM_ID5_0_1_,
members0_.id as id1_0_1_,
members0_.id as id1_0_0_,
members0_.age as age2_0_0_,
members0_.name as name3_0_0_,
members0_.TEAM_ID as TEAM_ID5_0_0_,
members0_.type as type4_0_0_
from
Member members0_
where
members0_.TEAM_ID in (
?, ?
)
글로벌 세팅으로도 가능
실무에서 보통 글로벌로 쓴다고 한다
persistence.xml
<property name="hibernate.default_batch_fetch_size" value="100" />
JPQL 다형성 쿼리
조회 대상을 특정 자식으로 한정
- 예) Item 중에 Book, Movie를 조회해라
JPQL
select i from Item i
where type(i) in (Book, Movie)
- SQL
select i.* from Item i
where i.dtype in ('Book', 'Movie')
JPQL 다형성 쿼리 : TREAT (JPA 2.1)
- 자바의 타입 캐스팅과 유사
- 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT (하이버네이트 지원) 사용
- JPQL
select i from Item i
where treat(i as Book).author = 'kim'
- SQL
/* select
i
from
Item i
where
treat(i as Book).author = '영한 킴' */ select
item0_.ITEM_ID as ITEM_ID2_2_,
item0_.name as name3_2_,
item0_.price as price4_2_,
item0_1_.artist as artist1_0_,
item0_2_.author as author1_1_,
item0_2_.isbn as isbn2_1_,
item0_3_.actor as actor1_3_,
item0_3_.director as director2_3_,
item0_.DTYPE as DTYPE1_2_
from
Item item0_
left outer join
Album item0_1_
on item0_.ITEM_ID=item0_1_.ITEM_ID
inner join
Book item0_2_
on item0_.ITEM_ID=item0_2_.ITEM_ID
left outer join
Movie item0_3_
on item0_.ITEM_ID=item0_3_.ITEM_ID
where
item0_2_.author='영한 킴'
엔티티 직접 사용 - 기본 키 값
JPQL 에서 엔티티를 직접 파라미터로 넣으면 해당 엔티티의 PK를 사용하게 된다
PK 기본 키
- JPQL
select m from Member m where m = :findMember
- SQL
select m.* from Member m where m.id = ?
FK 외래 키
- JPQL
select m from Member m where m.team = :findTeam
- SQL
select m.* from Member m
inner join team t
on m.team_id = t.id
where t.id = ?
JPQL Named 쿼리 - 정적 쿼리
미리 정의해서 이름을 부여하고 사용하는 JPQL
어노테이션, XML에 정의
정적 쿼리
- 애플리케이션 로딩 시점에 초기화 후 재사용
- 해당 시점에 검증하기 때문에 문법 오류를 조기에 파악 가능
spring data jpa에 더 간단하게 사용 할 수 있는 어노테이션이 있다
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.at-query
JPQL 벌크 연산
- 쿼리 한 번으로 여러 테이블 로우 변경
여러개를 insert, update, delete를 하고자 하는데 하나의 변경 건
에 하나의 쿼리
가 나가는 것은 비용이 비싸다!
여러 변경 건에 대해 처리를 하고자 하는 것 이게 벌크 !
예시 코드
String query = "update Member m set m.age = m.age +1 ";
int resultCount = em.createQuery(query).executeUpdate(); // updated row
out.println("resultCount = " + resultCount);
벌크 연산 주의 !
영속성 컨텍스트를 무시하고 DB에 바로 쿼리
벌크 연산 수행 후 영속성 컨텍스트를 초기화 하자 !
벌크 연산시 문제의 상황
회원 테이블의 나이를 +1 씩 한 후에 다시 조회했더니 이전의 값이 남아 있는 상황 !
Member userA = Member.builder()
.name("user a")
.age(20)
.build();
Member userB = Member.builder()
.name("user b")
.age(20)
.build();
em.persist(userA);
em.persist(userB);
String query = "update Member m set m.age = m.age +1 ";
int resultCount = em.createQuery(query).executeUpdate();
out.println("resultCount = " + resultCount);
Member findUserA = em.find(Member.class, userA.getId());
out.println("findUserA = " + findUserA);
- 출력 결과
findUserA = Member(id=4028b8817ea3cf87017ea3cf8a2a0000, name=user a, age=20, type=null)
조회 결과가 갱신된 나이인 21살이 아닌 20살이 출력된다
find 앞에서 캐시를 초기화 해주어야 한다 !
clear()
!
완강
드디어 ㅎㅎ 모두 완강하였다
조원분들 덕분이다..
단순히 강의를 들을 뿐만 아니라 여러 상황들에 대해 의견도 나누고 실험도 해보면서 진행하느라 힘들었던 것 같다.. ㅎㅎ
덕분에 jpa 이론은 한동안 계속 머리 속에 잠재되어 있을 것 같다.. 조원분들에게 감사하다 ㅎ