JPQL 중급 문법

D A S H B O A R D
D E V E L O P
S E C U R I T Y
 경로 표현식
 패치 조인(fetch join)***
 Join Fetch 특징과 한계
 다형성 쿼리
 엔티티 직접 사용
 Named 쿼리
 벌크 연산
Reference

 경로 표현식

경로표현식을 알면 JPQL이 어떻게 SQL로 변환되는 지 명확히 이해할 수 있다!!
.(점)을 찍어 객체 그래프를 탐색하는 것
select m.username // 상태 필드 from Member m join m.team t // 단일 값 연관 필드 join m.orders o // 컬렉션 값 연관 필드 where t.name = '팀A'
SQL
복사
태 필드(state field): 단순히 값을 저장하기 위한 필드 (ex: m.username)
연관 필드(association field): 연관관계를 위한 필드
단일 값 연관 필드: → @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team)
컬렉션 값 연관 필드: → @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders)

특징

상태필드 : 경로 탐색의 끝, 탐색X
em.createQuery("select m.username from Member m ")
Java
복사
해당 쿼리를 보낼 경우 m.username 이외에 더 탐색하지 않는다.
앞의 글에서 Member Class가 있는데, 이는 id, username, team으로 이루어져 있다고 보면 되는데, username은 String 타입으로 더 나아갈 곳이 존재하지 않는다 → m.username.~~ 이 존재하지 않다는 의미
단일 값 연관 경로 : 묵시적 내부 조인(inner join) 발생, 탐색O ⇒ 굉장히 중요!!
em.createQuery("select m.team from Member m ") em.createQuery("select m.team.id from Member m ") em.createQuery("select m.team.name from Member m ")
Java
복사
해당 쿼리를 보낼 경우 묵시적 조인이 발생하며 더 탐색할 수 있다. → 이는 JPQL 쿼리 상 join과 관련된 내용은 없지만, 실제로 쿼리가 나가게 된다면 join 쿼리가 실행된다는 의미
m.team을 보면 team은 하나의 엔티티이기 때문에 team 안으로 들어가 더 조회할 수 있다. → m.team.name과 같이 team의 이름을 조회하는 것까지 가능하다는 의미
여기서 m.team.name, m.team.id와 같이 더 나아가면 탐색의 끝이 되게 때문에, 상태 필드가 된다.
컬렉션 값 연관 경로 : 묵시적 내부 조인 발생, 탐색X
em.createQuery("select t.members from Team t ")
Java
복사
해당 쿼리는 더 이상 탐색을 하지 못한다.
만약 JPQL을 잘 모른다고 가정한다면, t.members.~ 가 가능하다고 생각할 수 있지만, 보통 컬렉션은 OneToMany 관계이기 때문이다. → 이는 반환 값이 Collection.Class이기 때문에 더 이상 탐색할 수 없다. → size는 가능!
만약, t.members에서 꼭 가져와야겠다면 방법은 있다. → FROM 절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하긴 하다!
em.createQuery("select m.username from Team t join t.members m")
Java
복사
명시적 조인 VS 묵시적 조인
명시적 조인: join 키워드 직접 사용
select m from Member m join m.team t
묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
select m.team from Member m
외부 조인을 하고 싶다면 명시적 조인을 사용해야 한다. 묵시적 조인은 내부 조인만 가능!
실무에서는 묵시적 JOIN이 발생하지 않도록 JPQL을 작성해야 한다.
묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어려움
추후, 가공할 때의 있어서의 어려움(조인은 SQL 튜닝에 중요 포인트)
유지보수의 어려움 등이 존재

 패치 조인(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
[SQL] : SELECT M.*, T.* FROM MEMBER MINNER JOIN TEAM T ON M.TEAM_ID=T.ID
join fetch의 쿼리를 보면 전에 배웠던 즉시로딩과 같다! → 해당 쿼리를 작성해서 사용한다면, 지연 로딩이라도 무시하고 해당 쿼리를 보낸다.

예시

1. 문제점 (N+1 문제)

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()); }
Java
복사
select m form Member m 이라는 JPQL을 작성했을 시에 Member 엔티티에 있는 team이 ManytoOne으로 작성되어 있고, fetchType이 LAZY로 설정되어 있다. → Team을 Proxy 엔티티로 가지고 온다.
그렇기 때문에, 첫 JPQL을 날렸을 때에는 Member 객체만 조회한 후
member.getTeam().getName() 을 호출했을 때 Team에 대한 쿼리를 날린다.
여기서 TeamA가 2명, TeamB가 1명이 있는데,
TeamA가 한번 조회된 후 영속성 컨텍스트에 저장되기 때문에 쿼리 한번
member = 회원1, 팀A ⇒ SQL 쿼리를 통해
member = 회원2, 팀A ⇒ 영속성 컨텍스트를 통해
TeamB는 영속성 컨텍스트에 없기 때문에 쿼리 한번이 나가
총 2번의 쿼리가 더 실행된다.
이러한 문제가 만약 회원이 100명이라면?!?
최악의 경우에는 쿼리가 100번 나가게된다.
이를 N +1 문제라고 한다. → 회원을 가지고 오는 쿼리 1번, 팀을 조회하기 위해 보내는 쿼리 N번(최악의 경우)이기 때문에 N+1 이라고 한다.

2. 해결책

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()); }
Java
복사
join fetch 를 통해 한방 쿼리를 실행한다.
이 때는 이미 join을 통해 영속성 컨텍스트에 Team과 Member 모두가 저장되기 때문에, Proxy로 받아오지 않는다!
위의 N+1 문제를 해결하면서 성능을 아주 크게 끌어 올릴 수 있다!!

컬렉션 패치 조인

일대다 관계, 컬렉션 페치 조인
[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()); }
Java
복사
하지만 나는 팀A, 팀B 딱 2줄만 나온다….
이유 : 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용됩니다.
해당 구문을 쿼리 했을 때 OneToMany 관계에서는 DB 로직에 의해 데이터가 뻥튀기 되서 옆에 결과 처럼 나오게 된다.
팀A는 한개지만, 멤버는 2명이기 때문에, DB 입장에서 Join을 하면 두 줄이 된다.
JPA입장에서 해당 부분을 처리하기에는 회원이 몇명인지 미리 알 수 없기 때문에 어떻게 할 수 없다. → 따라서 그냥 가지고 오게 된다.
하지만 영속성 컨텍스에 올라갈 때는, PK가 1번인 것 하나만 올라가게 된다.

패치 조인과 중복제거 DISTINCT

SQL의 DISTINCT는 중복된 결과를 제거하는 명령
JPQL의 DISTINCT 2가지 기능 제공
1. SQL에 DISTINCT를 추가
2. 애플리케이션에서 엔티티 중복 제거

예시

String query = "select DISTINCT 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()); }
Java
복사
SQL에 DISTINCT를 추가하지만 데이터가 다르므로 SQL 결과에서 중복제거 실패
SQL에서는 모든 항이 같아야 중복제거가 된다.
하지만 해당 테이블에서는 Member의 ID와 NAME이 다르기 때문에 중복제거가 되지 않는다.
그렇기 때문에 JPA가 추가로 애플리케이션에서 중복 제거시도
같은 식별자를 가진 Team 엔티티 제거

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

일반 조인 실행시 연관된 엔티티를 함께 조회하지 않음
[JPQL] : select t from Team t join t.members m where t.name = ‘팀A'
[SQL] : SELECT T.*FROM TEAM T INNER JOIN MEMBER M ON T.ID=M.TEAM_ID WHERE T.NAME = '팀A'
JPQL은 결과를 반환할 때 연관관계 고려X
단지 SELECT 절에 지정한 엔티티만 조회할 뿐
페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념

JOIN FETCH

JOIN

로딩 시점에 member에 대한 데이터가 영속성 컨텍스트에 저장되지 않는다.
즉, 이후에 사용될 때 쿼리를 통해 가지고오게 됨 → Proxy랑 비슷하지만 컬렉션은 Proxy가 아니다.(동작은 Proxy와 동일)

 Join Fetch 특징과 한계

페치 조인 대상에는 별칭을 줄 수 없다.
하이버네이트는 가능, 가급적 사용X
둘 이상의 컬렉션은 페치 조인 할 수 없다.
컬렉션을 페치 조인하면 페이징 API(setFirstResult,setMaxResults)를 사용할 수 없다
일대일, 다대일같은단일값연관필드들은페치조인해도페이징가능
하이버네이트는 경고 로그를 남기고 메모리에서 페이징 (매우 위험)
연관된 엔티티들을 SQL 한 번으로 조회 - 성능 최적화
엔티티에 직접 적용하는 글로벌 로딩 전략보다 우선함
@OneToMany(fetch = FetchType.LAZY) //글로벌 로딩 전략
실무에서 글로벌 로딩 전략은 모두 지연 로딩
@BatchSize() 이용 시 각각 개별적으로 사용 가능
최적화가 필요한 곳은 페치 조인 적용
페이징에 있어서 N+1 문제가 발생하는데, 이를 해결하는 방법으로 크게 3가지 존재
1.
join fetch 사용
2.
컬렉션의 경우 페이징이 불가하기 때문에, batch size 이용
@BatchSize(size = 100) @OneToMany(mappedBy = "team") private List<Member> members = new ArrayList<>();
Java
복사
<property name="hibernate.default_batch_fetch_size" value="100"/> <!-- 적절히 1000 이하로-->
XML
복사
3.
DTO로 직접 쿼리 작성!!
정리
모든 것을 페치 조인으로 해결할 수 는 없음
페치 조인은 객체 그래프를 유지할 때 사용하면 효과적
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른결과를 내야 하면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적

 다형성 쿼리

TYPE
조회 대상을 특정 자식으로 한정
예) Item 중에 Book, Movie를 조회해라
[JPQL] : select i from Item i where type(i) IN (Book, Movie)
[SQL] : select i from i where i.DTYPE in (‘B’, ‘M’)
TREAT(JPA 2.1)
자바의 타입 캐스팅과 유사
상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용
FROM, WHERE, SELECT(하이버네이트 지원) 사용
예) 부모인 Item과 자식 Book이 있다.
[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에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용
[JPQL]
select count(m.id) from Member m ⇒ 엔티티의 아이디를 사용
select count(m) from Member m ⇒ 엔티티를 직접 사용
[SQL] (JPQL 둘다 같은 다음 SQL 실행)
select count(m.id) as cnt from Member m

엔티티 전달 - 기본 키

String jpql = “select m from Member m where m = :member”; List resultList = em.createQuery(jpql) .setParameter("member", member) .getResultList();
Java
복사

식별자를 직접 전달 - 기본 키

String jpql = “select m from Member m where m.id = :memberId”; List resultList = em.createQuery(jpql) .setParameter("memberId", memberId) .getResultList();
Java
복사

실행될 쿼리

select m.* from Member m where m.id=?
SQL
복사
외래키 값

엔티티 전달 - 외래키

Team team = em.find(Team.class, 1L); String qlString = “select m from Member m where m.team = :team”; List resultList = em.createQuery(qlString) .setParameter("team", team) .getResultList();
Java
복사

식별자를 직접 전달 - 외래키

String qlString = “select m from Member m where m.team.id = :teamId”; List resultList = em.createQuery(qlString) .setParameter("teamId", teamId) .getResultList();
Java
복사

실행될 쿼리

select m.* from Member m where m.team_id=?
SQL
복사

 Named 쿼리

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

Named 쿼리 - 어노테이션

@Entity @NamedQuery( name = "Member.findByUsername", query="select m from Member m where m.username = :username") public class Member {... }
Java
복사
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class) .setParameter("username", "회원1") .getResultList();
Java
복사

Named 쿼리 - XML에 정의

[META-INF/persistence.xml]
<persistence-unit name="jpabook" > <mapping-file>META-INF/ormMember.xml</mapping-file>
XML
복사
XML 정의가 항상 우선권을 가짐!
XML이 항상 우선권을 가진다.
애플리케이션 운영 환경에 따라 다른 XML을 배포할 수 있다.
[META-INF/ormMember.xml]
<?xml version="1.0" encoding="UTF-8"?> <entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm" version="2.1"> <named-query name="Member.findByUsername"> <query><![CDATA[ select m from Member m where m.username = :username ]]></query> </named-query> <named-query name="Member.count"> <query>select count(m) from Member m</query> </named-query> </entity-mappings>
XML
복사

 벌크 연산

문제

재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행
1.
재고가 10개 미만인 상품을 리스트로 조회한다.
2.
상품 엔티티의 가격을 10% 증가한다.
3.
트렌젝션 커밋 시점에 변동 감지가 작동한다.
변경된 데이터가 100건이라면 100번의 UPDATE SQL 실행

해결

쿼리 한 번으로 여러 테이블 로우 변경(엔티티)
executeUpdate()의 결과는 영향받은 엔티티 수 반환
UPDATE, DELETE 지원
INSERT(insert into .. select, 하이버네이트 지원)
주의할 점!
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리 (commit, query, flush 시에 SQL쿼리가 나가게 된다.)
벌크 연산을 먼저 실행
벌크 연산 수행 후 영속성 컨텍스트 초기화
벌크 연산시 쿼리가 나가게 되는데, 그렇게 되면 DB는 업데이트가 되도, 영속성 컨텍스트는 업데이트가 되지 않는다.
그렇기 때문에, 영속성 컨텍스트를 초기화해준 후 새로 DB에서 데이터를 가지고 와야 한다.