JPQL - 경로 표현식
.(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -> 상태 필드 (Member의 username을 가리킴)
from Member m
join m.team t -> 단일 값 연관 필드 (Member에서 Team(엔티티)으로 넘어감)
join m.orders o -> 컬렉션 값 연관 필드 (Member에서 Orders(컬렉션)으로 넘어감)
where t.name = '팀A'
경로 표현식 용어 정리
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드(ex: m.username). 경로 탐색의 끝으로 더 이상 탐색 불가
- 연관 필드(association field) : 연관관계를 위한 필드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team). 묵시적으로 내부 조인(inner join)이 발생하며 더 탐색이 가능하다(ex: m.team.name).
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders). 묵시적으로 내부 조인이 발생하지만, 더 이상 탐색 불가(컬렉션에서는 필드를 찍을 수 없음). 탐색을 하고 싶다면 명시적인 조인을 사용해야 한다.("select m.username(탐색 가능) From Team t join t.members m")
단, 묵시적 내부 조인이 발생하도록 코드를 작성하지 마라. 명시적 조인을 사용해라.
실무 조언
묵시적 조인은 조인이 일어나는 상황을 한 눈에 파악하기 어려우므로 가급적 묵시적 조인 대신에 명시적 조인을 사용해라. 조인은 SQL 튜닝에 중요 포인트이다.
경로 탐색에 따른 JPQL → SQL
상태 필드 경로 탐색
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
-> SQL로 변환되면서 묵시적 조인이 발생함
명시적 조인, 묵시적 조인
- 명시적 조인 : join 키워드를 직접 사용
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
명시적 조인 : select m from Member m join m.team t
묵시적 조인 : select m.team from Member m
경로 표현식 예제
- select o.member.team from Order o → 성공
- select t.members from Team → 성공(컬렉션으로 members 까지만 접근했기 때문)
- select t.members.username from Team t → 실패(컬렉션에서는 members까지만 접근 가능)
- select m.username from Team t join t.members m → 성공(컬렉션을 명시적 조인으로 하여 별칭 m으로 username에 접근했기 때문)
JPQL - 페치 조인(fetch join) [중요]
- SQL 조인 종류 아님.
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 명령어 사용
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인(@ManyToOne)
SQL 한 번에 회원을 조회하면서 연관된 팀도 함께 조회하게 된다. SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT 한다.
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

// 페치 조인을 사용하지 않으면 아래와 같은 N + 1 문제 발생
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for(Member member: members) {
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
System.out.println("username = " + member.getUsername() + "," +
"teamName = " + member.getTeam().name());
}
/*
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
*/
N + 1 문제
1은 첫 번째에 날린 쿼리로 회원을 가져오기 위한 쿼리이고, N은 이게 100번 반복되면서 첫 번째 쿼리로 날린 결과로 얻은 결과만큼 n번 날리게 되는 것을 의미함. 즉시 로딩, 지연 로딩 모두 발생하므로 페치 조인으로 해결해야 함.
String query = "select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member m : result) { // 회원의 이름과 팀을 같이 출력 - 지연로딩 발생
System.out.println("member = " + m.getUsername() + ", " + m.getTeam().getName());
// 회원1, 팀A(SQL)
// 회원2, 팀A(1차 캐시)
// 회원3, 팀B(SQL)
// -> 쿼리가 3번 나감. 회원이 100명이라면? N + 1 발생
}
컬렉션 페치 조인
일대다 관계(@OneToMany)나 컬렉션 페치 조인을 하는 것으로 엔티티 페치 조인과 마찬가지로 join fetch 키워드로 조인할 경우, 페치 조인이 이루어져 N+1 문제를 막을 수 있다.
String query = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team t : result) {
System.out.println("team = team.getName() + "members = " + team.getMembers().size());
}
/*
team = 팀A members = 2 -> 중복 출력됨
team = 팀A members = 2 -> 중복 출력됨
team = 팀B members = 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'
*/
하지만 컬렉션 페치 조인의 경우, 위 주석의 출력값과 같이 데이터가 중복되어 조회되는 것을 확인할 수 있다.

일대다 관계에서 두 테이블이 조인될 경우에는 위 사진과 같이 ID=1에 해당되는 데이터는 중복되어 출력된다. JPA에서는 DB에 조회한 결과 개수만큼 그대로 돌려주는 것이다. 따라서 팀A를 조회하면 조회한 컬렉션에는 같은 주소값을 가진 결과가 2개가 있으므로 fetch join을 하게되면 팀A 입장에서는 회원1, 회원2를 가지고 있게 된다.
중복을 제거하고 싶다면? DISTINCT 사용 → 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
SQL의 DISTINCT는 중복된 결과를 제거하는 명령으로 JPQL의 DISTINCT는 2가지 명령을 제공한다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
String query = "select distinct t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team t : result) {
System.out.println("team = team.getName() + "members = " + team.getMembers().size());
}
각 팀에 소속된 회원 이름이 다르기 때문에 SQL에 distinct 키워드를 추가해도 DB 단에서는 중복이 제거되지 않는다.
하지만 JPQL에 DISTINCT 추가함으로 애플리케이션에서 중복 제거 시도를 하면 같은 식별자를 가진 Team 엔티티를 제거한다.

단, 반대의 경우인 다대일(@ManyToOne) 관계에서는 중복이 발생하지 않는다.
페치 조인 vs 일반 조인
일반 조인
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은 결과를 반환할 때 연관관계를 고려하지 않음. SELECT 절에 지정한 엔티티만 조회함
페치 조인
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'
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다. 하이버네이트는 가능하나, 가급적 사용하면 안된다.
- 둘 이상의 컬렉션은 페치 조인 할 수 없다. 둘 이상의 컬렉션을 조인하면 데이터가 엉켜서 뻥튀귀 된다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다. 일대일과 다대일과 같은 경우, 본인이 다수 측이고, 대상은 1이기 때문에 패치 조인을 해도 원하는 데이터의 수만큼만 나오게 된다.
- 하이버네이트는 동작은 하지만 경고 로그를 남기고 메모리에서 페이징한다.(절대로 사용하면 안됨) 만약, 컬렉션 페치 조인에서 페이징 처리를 하고 싶다면 두 가지 방법이 있다.
- 방법 1 : 페치 조인 방향 뒤집기
- 방법 2 : 컬렉션 필드에 @BatchSize(size = ?) 어노테이션 추가하기
모든 것을 페치 조인으로 해결할 수는 없으며 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블들을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
JPQL - 다형성 쿼리

TYPE
조회 대상을 특정 자식으로 한정시킬 수 있다. 예시로 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 ('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]
JPQL - 엔티티 직접 사용
기본 키 값
JPQL에서 엔티티를 직접 사용하려면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
[JPQL]
SELECT COUNT(m.id) FROM Member m # 엔티티의 아이디를 사용
SELECT COUNT(m) FROM Member m # 엔티티를 직접 사용
[SQL] → 둘 다 같은 SQL이 실행된다.
SELECT COUNT(m.id) as cnt FROM Member m
엔티티를 파라미터로 전달하나 식별자를 직접 전달하나 실행된 SQL을 확인하면 두 방식 모두 동일한 SQL이 생성된다.
/* 엔티티를 파리미터로 전달 */
String jpql1 = "SELECT m FROM Member m WHERE m = :member";
List resultList = em.createQuery(jpql1)
.setParameter("member", member)
.getResultList();
/* 식별자를 직접 전달 */
String jpql2 = "SELECT m FROM Member m WHERE m.id = :memberId";
List resultList = em.createQuery(jpql2)
.setParameter("memberId", memberId)
.getResultList();
-> 위 두 방식 모두 동일한 SQL이 생성됨
SQL : SELECT m.* FROM Member m WHERE m.id=?
외래 키 사용
@JoinColumn의 외래키 값에 (name=?)의 ?를 가리킨다.
// :team은 PK이고, m.team은 DB 입장에서 FK와 매핑이 되기 때문에 외래키로 인정된다.
String jpql = "SELECT m FROM Member m WHERE m.team = :team";
List resultList = em.createQuery(jpql)
.setParameter("team", teamA)
.getResultList();
String jpql = "SELECT m FROM Member m WHERE m.team.id = :teamId";
List resultList = em.createQuery(jpql)
.setParameter("teamId", team.getId())
.getResultList()
SQL : SELECT m.* FROM Member m WHERE m.TEAM_ID=?
JPQL - Named 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL로 정적 쿼리이다.
- 어노테이션으로 등록하거나, XML에 정의하여 사용한다.
- 애플리케이션 로딩 시점에 초기화 후 재사용한다.
- JPQL이 SQL로 파싱되는 과정이 애플리케이션 로딩 시점에 이루어지고, 해당 시점에 파싱한 후 캐싱해두고 호출될 때마다 가져다 쓴다.
- 애플리케이션 로딩 시점에 쿼리를 검증한다. -> 개발 과정에서 JPQL 오류를 컴파일 과정에서 확인할 수 있음
/* Named 쿼리 등록 */
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member {
...
}
/* Named 쿼리 사용 */
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
JPQL - 벌크 연산
일반적인 SQL의 UPDATE나 DELETE문으로 생각하면 되며 2개 이상의 데이터에 적용하는 것이다.
예시 : 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면? JPA가 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행된다.
- 재고가 10개 미만인 상품을 리스트로 조회하고
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경 감지가 동작한다.
변경된 데이터가 100건이라면, 100번의 UPDATE SQL이 실행된다. 이러한 문제점을 보완하기 위한 기능이 벌크 연산이다.
벌크 연산은 쿼리 한 번으로 여러 테이블의 Row를 변경한다. executeUpdate()의 결과는 영향받은 엔티티의 수를 반환한다.
UPDATE, DELETE를 지원하며 하이버네이트의 경우 INSERT(insert into .. select) 도 지원한다.
String jpql = "UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount";
int resultCount = em.createQuery(jpql)
.setParameter("stockAmount", 10)
.executeUpdate();
벌크 연산 주의
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하기 때문에 잘못하며 꼬이게 된다. 따라서 아래 중 하나로 풀어야 안전하다.
- 방법1 : 영속성 컨텍스트에 저장되기 전에 벌크 연산 먼저 실행 - 영속성 컨텍스트가 갱신되지 않아 발생하는 문제를 사전에 차단
- 방법2 : 이미 영속성 컨텍스트에 있다면, 벌크 연산 수행 후 영속성 컨텍스트 초기화 - 영속성 컨텍스트의 값은 최신 값이 아니므로 영속성 컨텍스트를 초기화한다. 그런 후에 해당 데이터가 필요하면 다시 조회
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 11장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA 활용 2] 2. API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2024.08.15 |
---|---|
[JPA 활용 2] 1. API 개발 기본 (0) | 2024.08.12 |
[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.23 |
[JPA 기본편] 9. 값 타입 (0) | 2024.06.21 |
[JPA 기본편] 8. 프록시와 연관관계 관리 (0) | 2024.05.22 |
JPQL - 경로 표현식
.(점)을 찍어 객체 그래프를 탐색하는 것
select m.username -> 상태 필드 (Member의 username을 가리킴)
from Member m
join m.team t -> 단일 값 연관 필드 (Member에서 Team(엔티티)으로 넘어감)
join m.orders o -> 컬렉션 값 연관 필드 (Member에서 Orders(컬렉션)으로 넘어감)
where t.name = '팀A'
경로 표현식 용어 정리
- 상태 필드(state field) : 단순히 값을 저장하기 위한 필드(ex: m.username). 경로 탐색의 끝으로 더 이상 탐색 불가
- 연관 필드(association field) : 연관관계를 위한 필드
- 단일 값 연관 필드 : @ManyToOne, @OneToOne, 대상이 엔티티(ex: m.team). 묵시적으로 내부 조인(inner join)이 발생하며 더 탐색이 가능하다(ex: m.team.name).
- 컬렉션 값 연관 필드 : @OneToMany, @ManyToMany, 대상이 컬렉션(ex: m.orders). 묵시적으로 내부 조인이 발생하지만, 더 이상 탐색 불가(컬렉션에서는 필드를 찍을 수 없음). 탐색을 하고 싶다면 명시적인 조인을 사용해야 한다.("select m.username(탐색 가능) From Team t join t.members m")
단, 묵시적 내부 조인이 발생하도록 코드를 작성하지 마라. 명시적 조인을 사용해라.
실무 조언
묵시적 조인은 조인이 일어나는 상황을 한 눈에 파악하기 어려우므로 가급적 묵시적 조인 대신에 명시적 조인을 사용해라. 조인은 SQL 튜닝에 중요 포인트이다.
경로 탐색에 따른 JPQL → SQL
상태 필드 경로 탐색
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
-> SQL로 변환되면서 묵시적 조인이 발생함
명시적 조인, 묵시적 조인
- 명시적 조인 : join 키워드를 직접 사용
- 묵시적 조인 : 경로 표현식에 의해 묵시적으로 SQL 조인 발생(내부 조인만 가능)
명시적 조인 : select m from Member m join m.team t
묵시적 조인 : select m.team from Member m
경로 표현식 예제
- select o.member.team from Order o → 성공
- select t.members from Team → 성공(컬렉션으로 members 까지만 접근했기 때문)
- select t.members.username from Team t → 실패(컬렉션에서는 members까지만 접근 가능)
- select m.username from Team t join t.members m → 성공(컬렉션을 명시적 조인으로 하여 별칭 m으로 username에 접근했기 때문)
JPQL - 페치 조인(fetch join) [중요]
- SQL 조인 종류 아님.
- JPQL에서 성능 최적화를 위해 제공하는 기능
- 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능
- join fetch 명령어 사용
- 페치 조인 ::= [ LEFT [OUTER] | INNER ] JOIN FETCH 조인경로
엔티티 페치 조인(@ManyToOne)
SQL 한 번에 회원을 조회하면서 연관된 팀도 함께 조회하게 된다. SQL을 보면 회원 뿐만 아니라 팀(T.*)도 함께 SELECT 한다.
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

// 페치 조인을 사용하지 않으면 아래와 같은 N + 1 문제 발생
String jpql = "select m from Member m join fetch m.team";
List<Member> members = em.createQuery(jpql, Member.class)
.getResultList();
for(Member member: members) {
// 페치 조인으로 회원과 팀을 함께 조회해서 지연 로딩 X
System.out.println("username = " + member.getUsername() + "," +
"teamName = " + member.getTeam().name());
}
/*
username = 회원1, teamname = 팀A
username = 회원2, teamname = 팀A
username = 회원3, teamname = 팀B
*/
N + 1 문제
1은 첫 번째에 날린 쿼리로 회원을 가져오기 위한 쿼리이고, N은 이게 100번 반복되면서 첫 번째 쿼리로 날린 결과로 얻은 결과만큼 n번 날리게 되는 것을 의미함. 즉시 로딩, 지연 로딩 모두 발생하므로 페치 조인으로 해결해야 함.
String query = "select m from Member m";
List<Member> result = em.createQuery(query, Member.class)
.getResultList();
for (Member m : result) { // 회원의 이름과 팀을 같이 출력 - 지연로딩 발생
System.out.println("member = " + m.getUsername() + ", " + m.getTeam().getName());
// 회원1, 팀A(SQL)
// 회원2, 팀A(1차 캐시)
// 회원3, 팀B(SQL)
// -> 쿼리가 3번 나감. 회원이 100명이라면? N + 1 발생
}
컬렉션 페치 조인
일대다 관계(@OneToMany)나 컬렉션 페치 조인을 하는 것으로 엔티티 페치 조인과 마찬가지로 join fetch 키워드로 조인할 경우, 페치 조인이 이루어져 N+1 문제를 막을 수 있다.
String query = "select t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team t : result) {
System.out.println("team = team.getName() + "members = " + team.getMembers().size());
}
/*
team = 팀A members = 2 -> 중복 출력됨
team = 팀A members = 2 -> 중복 출력됨
team = 팀B members = 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'
*/
하지만 컬렉션 페치 조인의 경우, 위 주석의 출력값과 같이 데이터가 중복되어 조회되는 것을 확인할 수 있다.

일대다 관계에서 두 테이블이 조인될 경우에는 위 사진과 같이 ID=1에 해당되는 데이터는 중복되어 출력된다. JPA에서는 DB에 조회한 결과 개수만큼 그대로 돌려주는 것이다. 따라서 팀A를 조회하면 조회한 컬렉션에는 같은 주소값을 가진 결과가 2개가 있으므로 fetch join을 하게되면 팀A 입장에서는 회원1, 회원2를 가지고 있게 된다.
중복을 제거하고 싶다면? DISTINCT 사용 → 하이버네이트6 부터는 DISTINCT 명령어를 사용하지 않아도 애플리케이션에서 중복 제거가 자동으로 적용된다.
SQL의 DISTINCT는 중복된 결과를 제거하는 명령으로 JPQL의 DISTINCT는 2가지 명령을 제공한다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
String query = "select distinct t from Team t join fetch t.members";
List<Team> result = em.createQuery(query, Team.class)
.getResultList();
for (Team t : result) {
System.out.println("team = team.getName() + "members = " + team.getMembers().size());
}
각 팀에 소속된 회원 이름이 다르기 때문에 SQL에 distinct 키워드를 추가해도 DB 단에서는 중복이 제거되지 않는다.
하지만 JPQL에 DISTINCT 추가함으로 애플리케이션에서 중복 제거 시도를 하면 같은 식별자를 가진 Team 엔티티를 제거한다.

단, 반대의 경우인 다대일(@ManyToOne) 관계에서는 중복이 발생하지 않는다.
페치 조인 vs 일반 조인
일반 조인
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은 결과를 반환할 때 연관관계를 고려하지 않음. SELECT 절에 지정한 엔티티만 조회함
페치 조인
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'
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)
- 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념
페치 조인의 특징과 한계
- 페치 조인 대상에는 별칭을 줄 수 없다. 하이버네이트는 가능하나, 가급적 사용하면 안된다.
- 둘 이상의 컬렉션은 페치 조인 할 수 없다. 둘 이상의 컬렉션을 조인하면 데이터가 엉켜서 뻥튀귀 된다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResult)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다. 일대일과 다대일과 같은 경우, 본인이 다수 측이고, 대상은 1이기 때문에 패치 조인을 해도 원하는 데이터의 수만큼만 나오게 된다.
- 하이버네이트는 동작은 하지만 경고 로그를 남기고 메모리에서 페이징한다.(절대로 사용하면 안됨) 만약, 컬렉션 페치 조인에서 페이징 처리를 하고 싶다면 두 가지 방법이 있다.
- 방법 1 : 페치 조인 방향 뒤집기
- 방법 2 : 컬렉션 필드에 @BatchSize(size = ?) 어노테이션 추가하기
모든 것을 페치 조인으로 해결할 수는 없으며 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블들을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면, 페치 조인 보다는 일반 조인을 사용하고 필요한 데이터들만 조회해서 DTO로 반환하는 것이 효과적이다.
JPQL - 다형성 쿼리

TYPE
조회 대상을 특정 자식으로 한정시킬 수 있다. 예시로 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 ('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]
JPQL - 엔티티 직접 사용
기본 키 값
JPQL에서 엔티티를 직접 사용하려면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
[JPQL]
SELECT COUNT(m.id) FROM Member m # 엔티티의 아이디를 사용
SELECT COUNT(m) FROM Member m # 엔티티를 직접 사용
[SQL] → 둘 다 같은 SQL이 실행된다.
SELECT COUNT(m.id) as cnt FROM Member m
엔티티를 파라미터로 전달하나 식별자를 직접 전달하나 실행된 SQL을 확인하면 두 방식 모두 동일한 SQL이 생성된다.
/* 엔티티를 파리미터로 전달 */
String jpql1 = "SELECT m FROM Member m WHERE m = :member";
List resultList = em.createQuery(jpql1)
.setParameter("member", member)
.getResultList();
/* 식별자를 직접 전달 */
String jpql2 = "SELECT m FROM Member m WHERE m.id = :memberId";
List resultList = em.createQuery(jpql2)
.setParameter("memberId", memberId)
.getResultList();
-> 위 두 방식 모두 동일한 SQL이 생성됨
SQL : SELECT m.* FROM Member m WHERE m.id=?
외래 키 사용
@JoinColumn의 외래키 값에 (name=?)의 ?를 가리킨다.
// :team은 PK이고, m.team은 DB 입장에서 FK와 매핑이 되기 때문에 외래키로 인정된다.
String jpql = "SELECT m FROM Member m WHERE m.team = :team";
List resultList = em.createQuery(jpql)
.setParameter("team", teamA)
.getResultList();
String jpql = "SELECT m FROM Member m WHERE m.team.id = :teamId";
List resultList = em.createQuery(jpql)
.setParameter("teamId", team.getId())
.getResultList()
SQL : SELECT m.* FROM Member m WHERE m.TEAM_ID=?
JPQL - Named 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL로 정적 쿼리이다.
- 어노테이션으로 등록하거나, XML에 정의하여 사용한다.
- 애플리케이션 로딩 시점에 초기화 후 재사용한다.
- JPQL이 SQL로 파싱되는 과정이 애플리케이션 로딩 시점에 이루어지고, 해당 시점에 파싱한 후 캐싱해두고 호출될 때마다 가져다 쓴다.
- 애플리케이션 로딩 시점에 쿼리를 검증한다. -> 개발 과정에서 JPQL 오류를 컴파일 과정에서 확인할 수 있음
/* Named 쿼리 등록 */
@Entity
@NamedQuery(
name = "Member.findByUsername",
query = "SELECT m FROM Member m WHERE m.username = :username")
public class Member {
...
}
/* Named 쿼리 사용 */
List<Member> resultList = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
JPQL - 벌크 연산
일반적인 SQL의 UPDATE나 DELETE문으로 생각하면 되며 2개 이상의 데이터에 적용하는 것이다.
예시 : 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면? JPA가 변경 감지 기능으로 실행하려면 너무 많은 SQL이 실행된다.
- 재고가 10개 미만인 상품을 리스트로 조회하고
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경 감지가 동작한다.
변경된 데이터가 100건이라면, 100번의 UPDATE SQL이 실행된다. 이러한 문제점을 보완하기 위한 기능이 벌크 연산이다.
벌크 연산은 쿼리 한 번으로 여러 테이블의 Row를 변경한다. executeUpdate()의 결과는 영향받은 엔티티의 수를 반환한다.
UPDATE, DELETE를 지원하며 하이버네이트의 경우 INSERT(insert into .. select) 도 지원한다.
String jpql = "UPDATE Product p SET p.price = p.price * 1.1 WHERE p.stockAmount < :stockAmount";
int resultCount = em.createQuery(jpql)
.setParameter("stockAmount", 10)
.executeUpdate();
벌크 연산 주의
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리하기 때문에 잘못하며 꼬이게 된다. 따라서 아래 중 하나로 풀어야 안전하다.
- 방법1 : 영속성 컨텍스트에 저장되기 전에 벌크 연산 먼저 실행 - 영속성 컨텍스트가 갱신되지 않아 발생하는 문제를 사전에 차단
- 방법2 : 이미 영속성 컨텍스트에 있다면, 벌크 연산 수행 후 영속성 컨텍스트 초기화 - 영속성 컨텍스트의 값은 최신 값이 아니므로 영속성 컨텍스트를 초기화한다. 그런 후에 해당 데이터가 필요하면 다시 조회
[출처]
https://www.inflearn.com/course/ORM-JPA-Basic -> 이 글은 김영한님의 JPA 강의 중 11장을 듣고 정리한 내용입니다.
'Spring > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA 활용 2] 2. API 개발 고급 - 지연 로딩과 조회 성능 최적화 (0) | 2024.08.15 |
---|---|
[JPA 활용 2] 1. API 개발 기본 (0) | 2024.08.12 |
[JPA 기본편] 10. 객체지향 쿼리 언어1 - 기본 문법 (0) | 2024.06.23 |
[JPA 기본편] 9. 값 타입 (0) | 2024.06.21 |
[JPA 기본편] 8. 프록시와 연관관계 관리 (0) | 2024.05.22 |