경로 표현식
- .(점)을 찍어서 객체 그래프를 탐색하는 것
- 상태 필드(state field): 단순히 값을 저장하기 위한 필드(ex: m.username)
- 연관 필드: 연관관계를 위한 필드
- 단일 값 연관 필드:
@ManyToOne
,@OneToOne
, 대상이 엔티티(ex: m.team)
- 컬렉션 값 연관 필드:
@OneToMany
,@ManyToMany
, 대상이 컬렉션(ex: m.orders)
- 단일 값 연관 필드:
특징
- 상태 필드: 경로의 탐색이 끝이난다. 더 이상 탐색 불가능
- 단일 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색 가능
- 컬렉션 값 연관 경로: 묵시적 내부 조인(inner join) 발생, 탐색 불가능
- 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.memeber_id = m.id
명시적 조인, 묵시적 조인
- 명시적 조인: join 키워드를 직접 사용한다.
SELECT m FROM Member_m join m.team t
- 묵시적 조인: 경로 표현식에 의해 묵시적으로 SQL 조인이 발생(내부 조인만 발생한다)
SELECT m.team FROM Member_ m
실무 조언
- 가급적 묵시적 조인 대신 명시적 조인을 사용하자!
- 조인이 SQL 튜닝에 중요 포인트다.
- 묵시적 조인은 조인이 일어나는 상황을 한 눈에 파악하기 어렵다.
페치 조인
- 실무에서 가장 중요한 조인이다!
- SQL의 조인이 아니라 JPQL의 성능 최적화를 위해 제공하는 기능이다.
- 요약하면 연관된 엔티티나 컬렉션을 SQL 한 번에 함께 조회하는 기능이다.
엔티티 페치 조인
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());
}
- 회원 3명을 가져오는 SQL 1개
- 회원1의 team을 가져오는 SQL 1개
- 회원 2의 team은 1차 캐시에 저장 되어 있음
- 회원3의 team을 가져오는 SQL 1개
- 2개의 SQL이 추가로 발생해서 1+N문제가 발생하게 된다!
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());
}
- 회원 3개를 가져오는 SQL 1개만 실행된다!
- 이 SQL에서
Member_
와Team_
을 join해서 가져오기 때문에 지연로딩이 아니라 즉시로딩처럼 가져오게 된다.
- 이 SQL에서
컬렉션 페치 조인
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());
}
- 페치 조인을 통해서 지연로딩 없이 바로 즉시로딩 한다.
문제
- 일대다 관계에서 join을 해버리면 위 그림처럼 팀A가 2줄로 중복되게된다.
- 다대일 관계에서는 중복(뻥튀기)될 일이 없다!
- 물론 영속성컨텍스트(1차 캐시)에서는 같은 객체로 취급하지만, 어쨋든 쿼리의 결과 리스트에는 같은 객체가 2개 담겨있게 된다.
해결 방법(DISTINCT)
- JPQL의 DISTINCT는 2가지 기능을 제공한다.
- SQL에 DISTINCT를 추가
- 애플리케이션에서 엔티티 중복 제거
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() + ": " + team.getMembers().size());
}
- JPQL이 SQL에 DISTINCT를 추가하지만, 위 테이블에서 row들이 완전히 동일하지 않기 때문에 SQL자체의 결과는 이전과 똑같다(3개)
- 따라서, JPQL이 애플리케이션 레벨에서 엔티티 중복을 제거해서 반환해준다.
페치 조인과 일반 조인의 차이
JPQL: select distinct t FROM Team_ t join t.members
---
SQL: select
team_0_.id as id1_10_,
team_0_.name as name2_10_
from
Team_ team_0_
inner join
Member_ members1_
on team_0_.id=members1_.team_id
JPQL: select distinct t FROM Team_ t join fetch t.members
---
SQL: select
team_0_.id as id1_10_0_,
members1_.id as id1_5_1_,
team_0_.name as name2_10_0_,
members1_.age as age2_5_1_,
members1_.memberType as memberty3_5_1_,
members1_.team_id as team_id5_5_1_,
members1_.username as username4_5_1_,
members1_.team_id as team_id5_5_0__,
members1_.id as id1_5_0__
from
Team_ team_0_
inner join
Member_ members1_
on team_0_.id=members1_.team_id
- 일반 조인은 실행시 join은 하지만, 결과를 조회하지는 않는다.
- JPQL은 결과를 반환할 때 연관관계를 고려하지 않는다.
- 단지 SELECT절에 지정한 엔티티만 조회하게 된다.
- 여기서는 팀 엔티티만 조회하고, 회원 엔티티는 조회하지 않는다.
- 페치 조인을 사용할 때만 연관된 엔티티도 함께 조회(즉시 로딩)하게 된다.
- 즉, 페치 조인은 객체 그래프를 SQL 한 번에 조회하는 개념이다.
페치 조인 한계
- 페치 조인 대상에는 별칭을줄 수 없다.
select t FROM Team_ t join fetch t.members as m
- 하이버네이트에서 가능하긴 한데… 가급적 사용하지 말자.
- 별칭을 주는 이유는 m.xxx로 그래프 탐색하기 위해서인데, where나 on을 통해서 members를 필터링 하게 될 경우 일관성에 문제가 발생할 수 있다.
- 어떤 쿼리에서는 members를 3명만 가져오고, 어떤 쿼리에서는 members를 모두 가져와서 5명이 되면… 뭐가 진짜인지 모른다.
- 일관성에 문제 없는 순수 조회성 sql의 경우에는 별칭을 줘도 된다.
- 둘 이상의 컬렉션은 페치 조인 할 수 없다.
- 일대다만 해도 데이터가 뻥튀기되는데, 둘 이상의 컬렉션은 일대다대다로 엄청 뻥튀기되기 때문에 사용하면 절대 원하는 결과가 안 나온다.
- 컬렉션을 페치 조인하면 페이징 API(setFirstResult, setMaxResults)를 사용할 수 없다.
- 일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인을 해도 페이징 가능하다.
- 반면, 일대다인 컬렉션은 DISTINCT를 넣더라도 SQL자체는 데이터가 뻥튀기되기 때문에 페이징이 정상적으로 동작하지 않는다.
- 하이버네이트에서는 경고 로그를 남기고, SQL이 아닌 메모리 위에서 페이징하게 된다.
- SQL에서 모든 데이터를 싹 다 가져오고 메모리 위에서 페이징하기 때문에 장애가 발생할 가능성이 너무 높다.
페치조인 한계 극복(@BatchSize)
String query = "select t FROM Team_ t";
List<Team_> resultList = em.createQuery(query, Team_.class)
.setFirstResult(0)
.setMaxResults(2)
.getResultList();
@BatchSize(size = 100)
@OneToMany(mappedBy = "team")
private List<Member_> members = new ArrayList<>();
select
members0_.team_id as team_id5_5_1_,
members0_.id as id1_5_1_,
members0_.id as id1_5_0_,
members0_.age as age2_5_0_,
members0_.memberType as memberty3_5_0_,
members0_.team_id as team_id5_5_0_,
members0_.username as username4_5_0_
from
Member_ members0_
where
members0_.team_id in (
?, ?
)
- 페치조인을 사용하지 않고, 일단 LAZY로 가져온다.
- team 하나 조회할 때 마다 members를 하나씩 계속 조회하기 때문에 1+N문제가 발생한다.
- members에
@BatchSize
를 걸어주면 IN퀴리가 나가게 된다.
@BatchSize
만큼 team을 모아서 한 번에 넘기게 된다.
정리
- 글로벌 전략은 모두 LAZY로 하고, 1+N문제가 발생하는 곳에만 페치조인을 적용시키자.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 모양을 내야하면 일반 조인을 사용하고, DTO로 반환하는 것이 효과적이다.
- 엔티티 자체가 필요한 경우에 페치조인으로 가져오자.
다형성 쿼리
TYPE
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(JPA2.1)
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: select count(m.id) from Member_ m // 엔티티의 아이디를 사용
JPQL: select count(m) from Member_ m // 엔티티를 직접 사용
---
SQL: select count(m.id) as cnt from Member_ m // 동일한 SQL이 실행
- JPQL에서 엔티티를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
외래 키 값
JPQL: select m from Member_ m where m.team = :team // 엔티티를 직접 사용
JPQL: select m from Member_ m where m.team.id = :teamId // 엔티티의 아이디를 사용
---
SQL: select m.* from Member m where m.team_id = ?
- 연관 관계에 있는 엔티티를 직접 사용하면 외래키를 넘긴다.
Named 쿼리
- 미리 정의해서 이름을 부여해두고 사용하는 JPQL이다.
- 정적 쿼리만 가능하고, 어노테이션과 XML에 정의할 수 있다.
- 애플리케이션 로딩 시점에 초기화 후 재사용 하기 때문에 로딩 시점에 쿼리 검증이 가능하다.
@Entity
@NamedQuery(
name = "Member_.findByUsername",
query = "select m from Member_ m where m.username = :username"
)
public class Member_ {
...
}
List<Member_> resultList = em.createNamedQuery("Member_.findByUsername", Member_.class)
.setParameter("username", "회원1")
.getResultList();
for (Member_ member : resultList) {
System.out.println("member = " + member);
}
벌크 연산
- 만약 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
- JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행하게 된다.
- 재고가 10개 미만인 상품을 리스트로 조회한다.
- 상품 엔티티의 가격을 10% 증가한다.
- 트랜잭션 커밋 시점에 변경감지가 동작한다.
- 변경된 데이터가 100건 이라면 100번의 UPDATE SQL이 실행된다.
- JPA 변경 감지 기능으로 실행하려면 너무 많은 SQL 실행하게 된다.
예제
int resultCount = em.createQuery("update Member_ m set m.age = 20")
.executeUpdate();
System.out.println("resultCount = " + resultCount);
- 쿼리 한 번으로 여러 테이블의 row을 변경한다.
- executeUpdate()의 결과는 영향을 받은 엔티티 수를 반환한다.
- UPDATE, DELETE를 지원한다.
- 하이버네이트 기준 INSERT도 지원한다.
주의
em.createQuery("update Member_ m set m.age = 20")
.executeUpdate();
em.clear();
System.out.println("m1.getAge() = " + m1.getAge());
System.out.println("m2.getAge() = " + m2.getAge());
System.out.println("m3.getAge() = " + m3.getAge());
---
m1.getAge() = 0
m2.getAge() = 0
m3.getAge() = 0
- 벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다.
- 따라서, 벌크 연산을 먼저 실행하거나
- 벌크 연산 수행 후 영속성 컨텍스트를 초기화 한다.
- 벌크 연산 수행 시 벌크 연산도 쿼리이기 때문에
flush
는 된다.
flush
만 되기 때문에clear
는 따로 불러주자.
- 물론, 이전 엔티티는 사용하지 못 하고 다시 조회해주어야 DB의 값을 받아올 수 있다.
- 벌크 연산 수행 시 벌크 연산도 쿼리이기 때문에
Uploaded by N2T
(23.06.12 23:44)에 작성된 글 입니다.