JPA를 공부하자 05 - JPQL 중급편
경로표현식
.
(점)을 찍어 그래프를 탐색하는 것을 말한다.
m.username
상태필드 /// 단순히 값 저장m.team
단일 값 연관필드 // OneToOne, ManyToOnem.orders
컬렉션 연관필드 // OneToMany, ManyToMany
이렇듯, 세가지 경로 표현식있고
어떤 필드를 사용하냐에 따라 내부동작이 달라지므로 꼭 구분해서 사용하자.
<br>
상태필드
는 경로 탐색의 끝이다, 탐색을 더이상하지 않는다.
단일 값 연관 경로
는 묵시적으로 JOIN
쿼리가 발생하고, 더 깊은 탐색이 가능하다. ex) m.team.teamName
컬렉션 값 연관 경로
는 묵시적으로 JOIN
쿼리가 발생하지만, 더 깊은 탐색은 불가능하다. 명시적 조인을 통해 별칭을 얻어야 더 깊은 탐색이 가능하다.
ex) select t.members.username from Team t
> 불가
ex) select m.username from Team t join t.members m
> 가능
중요한 것은 가급적 묵시적 조인
이 아닌, 명시적 조인
을 사용해야 한다는 것이다.
앞서 언급햇듯이, JOIN
성능상의 이슈이고, 튜닝해야 하는 문제중에 하나이다. 의도치않게 JOIN
이 나가는 경우는 항상 주의해야한다.
<br>
페치 조인
페치 조인은 객체 그래프를 SQL 한번에 조회하는 개념
임을 기억하자.
예를들어, 회원을 조회하면서 팀을 항상 JOIN
해서 쓴다고 생각하자.
select m from Member m join m.team
을한다면,
Member
를 가져오고, 각 Member
당 Team
을 SELECT
하는 쿼리가 호출된다.
따라서 Member
가 N
개라면 N+1
의 쿼리가 호출된다.
페치 조인
은 이를 해결해준다.
select m from Member m join fetch m.team
로 조회한다면,
쿼리 호출시, JOIN
을 해서 조회한다. 따라서 1
번의 쿼리의 요청으로 데이터를 조회 할 수 있다.
<br>
컬렉션 또한 페치 조인
가능 하다.
select t from Team t join fetch t.members
로 조회한다면,
JOIN
을 해서 조회한다.
하지만 주의 해야 할 점이 있다.
JOIN
은 1:N
, N:M
관계에서는 중복되는 데이터가 발생하는 것이다.
JPQL의 DISTINCT
키워드는 이 문제를 해결해준다.
SQL의 DISTINCT
의 중복된 결과를 제거하는 것에 더해서,
애플리케이션 단에서 ENTITY
의 중복 또한 제거해주는 것이다.
select distinct t from Team t join fetch t.members
<br>
페치 조인의 한계
첫번째로, 페치조인에 대상에는 별칭을 줄 수 없다.
기본적으로 JPA 설계 사상 자체가, 연관된 값을 다가져오는 것을 기본으로하는 것을 명심해야한다.
따라서, 페치조인
이라는 것은 연관된 것을 다 가져와야하는 것이다.
다음 코드는 불가능하다, (가능하긴 하지만 쓰면안된다.)
select t from Team t join fetch t.members m
select t from Team t join fetch t.members m where m.age > 15
예를들어 A Team
에 Member
가 10명이 있다.
별칭을 주고 Member
에 조건을 주어, A Team
에 소속된 3명의 Member
만 가져온다.
그리고, 이번엔 조건을 주지않고, 10개의 Member
를 모두 가져온다.
똑같은 A Entity
임에도 불구하고, 데이터의 불일치가 발생하는 것이다.
이는 곧 문제점으로 이어질 것이다.
그럼에도 조건
을 주고 3명의 Member
만 가져오고싶다면,
3명을 조회하는 Member
를 따로 쿼리를 요청하도록하자.
<br>
두번째로, 둘 이상의 컬렉션은 페치 조인을 하면 안된다.
select t From Team t join fetch t.members m join fetch t.orders
1:N
도 중복된 곱하기가 N만큼 생기는데,
거기에 또 1:N
을 한다면, 또 곱하기 N을 하는 것이다.
그렇다면 많은 중복된 데이터가 발생하고, 지나친 성능 문제로 이어진다.
또, 잘못된 데이터가 나올 가능성도 있다.
<br>
세번째로, 컬렉션을 페치 조인하면 페이징 API를 사용 할 수 없다.
예를 들어 1:N
관계에서, 페이지 사이즈를 10
으로 주자.
하지만 컬렉션 페치 조인
을 하게 되면, 검색된 데이터의 row가 곱하기가 되버린다.
row
가 적다면 괜찮을 수도 있지만, 대량의 데이터라면,
어플리케이션에서 Entity DITINCT
를 하는데, 많은 리소스를 필요로 할 것이다.
따라서, 사용할 수 있지만, 사용한다면 WARNING
메세지를 확인 할 수 있다.
즉 컬렉션 페이조인
은 페이징 API에서 사용되지 않음이 권장되고, 또 그렇게 해야한다.
결국 페이징에서는 페치 조인
을 사용할 수 없고,
LAZY
하게 사용할 수 있으며, 따라서 N+1
의 문제가 발생한다.
이 문제는, 단순히 다음과 같이 해결 할 수도 있다.
1:N
을 N:1
로 뒤집는 것이다.
select Member From m t join fetch m.team
하지만, 이 문제를 해결 할 수 있는 좀더 명확한 방법은
BATCH SIZE
라는 개념을 사용하는 것이다.
default_batch_fetch_size
를 사용하여 기본 사이즈를 지정하거나
@BatchSize
를 사용하여 정의 할 수 있다.
예를들어 Team
을 10개를 조회한다면,
각 팀의 Member
를 조회하는 10개의 쿼리를 더 요청하게 된다.
하지만 BATCH SIZE
를 설정한다면 10개의 쿼리가 아닌,
한번의 쿼리로 요청을 해결할 수 있다.
select m from Member m where m.team.id in (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
BATCH_SIZE
만큼 Member
를 한 번에 조회하여,
메모리에 저장하여 가지고 있는 것이다.
각 Team
의 Member
를 조회할 때, 쿼리 요청이 아닌, 메모리에 저장된 값을 반환한다.
정리하면 다음과 같다.
- 모든 것을 페치 조인으로 해결 할 수가 없다.
- 페치 조인은 객체 그래프를 유지할 때 사용 하면 효과적이다.
- 여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야한다면, 페치 조인보다는 일반 조인을 사용하고 필요한 데이터만을 조회해서 DTO로 반환하는것이 효과적일 것이다.
<br>
다형성쿼리
다형성 쿼리는 크게 중요한 부분은아니다.
entity
를 상속관계로 정의 했을 경우 사용한다.
TYPE
조회 대상을 특정 자식
으로 다음과 같이 한정 할 수 있다
select i from item i
where type(i) IN (Book, Movie)
<br>
TREAT
자바의 하위 캐스팅과 비슷한 개념이다
selct i from Item i
where treat(i as Book).author = 'kim'
<br>
엔티티의 직접 사용
JPQL에서 엔티티를 직접 사용하면 SQL에서 어떻게 반영되는지를 보자.
select count(m.id) from Member m
select count(m) from Member m // 엔티티 직접사용
<br>
JPA는 JPQL을 번역하면서, 엔티티를 엔티티의 id
를 사용하여 번역 한다.
select count(m.id) from Member m
<br>
파라미터로 넘겨도 마찬가지로 id
를 사용한다.
String jpql "slect m from Member m where m = :member";
em.createQuery(jpql)
.setParamemeter("member", member) //엔티티 직접 사용
.getResultList();
select m.* from Member m where m.id = ?
<br>
Named Query
Named Query
는 JQPL Query를 미리 정의하고,
재사용 할 수 있도록 하는 것이다.
어노테이션, XML 방식(잘쓰지않음) 으로 정의 할 수 있다.
(우선순위는 항상 XML 방식이 높다)
중요한것은 Named Query
는 정적 쿼리라는 것이다.
이 말은 즉, 애플리케이션 로징 시점에 초기화가 된다는 것 이고,
다시 번역하면, 어플리케이션 로딩 시점에 쿼리를 검증 할 수가 있다.
쿼리에 문법 오류가 발생하면, 컴파일시 확인 할 수 있는 것이다.
@Entity
@NamedQuery(
name = "Member.namedQuery"
query = "select m from ~~~~"
public class Member{
}
em.createQuery("Member.namedQuery", Member.class)
.setParameter("~~~", "~~")
.gerResultList();
<br>
Spring Data JPA
는 이를 한번 더 추상화 시켰는데,
다음과 같은 방식으로 사용한다.
publci interface MemberRepository extends JpaRepository<Member, Long> {
@Query("select m from Member m ~~") // Named Query,
Member findByName(String name);
}
<br>
벌크연산
예를 들어, update
, delete
쿼리를 10000
개 쿼리에 요청하고 싶다면,
10000
개의 데이터를 가져와, dirty checking
방식을 사용해
10000
번의 쿼리를 요청해야한다.
이런 문제 때문에,
JPA도 한번의 쿼리로 해결 할 수 있는 방식을 제공하고
이를 벌크 연산
이라 정의 한다.
em.createQuery("update Member m set m.age = 10");
<br>
벌크 연산을 사용함에 있어서, 주의해야 할 점을 꼭 기억해야한다.
Member m = new Member();
m.setAge(20);
em.persist(m);
em.createQuery("update Member m set m.age = 10");
Member savedM = em.findById(m.getId());
System.out.println(savedM.getAge()); /// 10??? 20???
해당 값의 결과는 10
이 아닌, 20
을 보여줄 것이다.
벌크 연산은 영속성 컨텍스를 무시하고,
데이터베이스에 직접 쿼리를 요청하기 때문이다.
실제로 벌크 연산
쿼리가 요청되어, 데이터 베이스의 값
은 바뀌었을 것이다.
하지만, 영속성 컨텍스트 내의 1차 캐시
공간에, 값은 바뀌지 않았다.
이 때문에, 벌크 연산
은 영속성 컨텍스트의 1차 캐시
의 값과,
데이터베이스의 값
의 데이터 불일치가 발생 하게된다.
따라서, 1차 캐시
에 저장된 m
을 리턴하게되고, 20
이라는 값을 보여주게 되는 것이다.
<br>
그러므로, 벌크 연산
을 사용함에 있어 다음 규칙중 하나를 꼭 준수하자.
1. 무조건 벌크 연산을 우선 수행한다.
2. 벌크 연산
이후에는, em.clear() // 1차캐시 초기화
를 반드시 호출하자.
<br>
Spring Data Jpa
는 이 문제를 @Modifying
어노테이션을 붙여줌으로 해결한다.
insert
, update
, delete
벌크 연산의 쿼리의 경우
@Modifying
어노테이션이 강제규약이되며, 없을경우 컴파일 에러가 발생한다.
@Modifying
어노테이션이 붙음으로써, 해당 쿼리가 벌크연산
이라는 것을 명시하고, 쿼리 수행 후 영속성컨텍스트가 초기화된다.
(어노테이션의 cleareAutomatically
값을 통해 영속성컨텍스트를 초기화 안시킬수도 있다).
publci interface MemberRepository extends JpaRepository<Member, Long> {
@Modifying
@Query("update Member m set m.age = :age")
Member updateMemberAge(Integer age);
}