JPQL
경로 표현식
.(점)을 찍어 객체 그래프를 탐색하는 것
상태 필드 : 단순히 값을 저장하기 위한 필드
연관 필드 : 연관관계를 위한 필드, 임베디드 타입 포함
단일 값 연관 필드 :
@ManyToOne,@OneToOne, 대상이 엔티티컬렉션 값 연관 필드 :
@OneToMany,@ManyToMany, 대상이 컬렉션
특징
상태 필드 경로 : 경로 탐색의 끝이다. 더 이상 탐색할 수 없다.
단일 값 연관 경로 : 묵시적 내부 조인이 발생한다. 계속 탐색할 수 있다.
컬렉션 값 연관 경로 : 묵시적 내부 조인이 발생한다. 더 이상 탐색할 수 없다.
FROM절에서 명시적 조인을 통해 별칭을 얻으면 별칭을 통해 탐색이 가능하다.select t.members.name form Team t실패한다.select m.name from Team t join t.members m성공한다.
명시적 조인 :
JOIN을 직접 적어준다.select m from Member m join m.team t
묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어난다. 내부 조인만 할 수 있다.
select m.team from Member m
가급적 묵시적 조인 대신에 명시적 조인을 사용하자. 묵시적 조인은 조인이 일어나는 상황을 한눈에 파악하기 어렵다.
페치 조인
SQL의 조인 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
연관된 엔티티나 컬렉션을 한 번에 같이 조회하는 기능이다.
엔티티 페치 조인
List<Member> result = em.createQuery("select m from Member m join fetch m.team", Member.class)
.getResultList();
for (Member member : result) {
System.out.println("member = " + member.getName() + ", " + member.getTeam().getName());
}join fetch없이 select m from Member m인 상태에서 객체 그래프 탐색을 하면 지연로딩(LAZY)으로 설정되어 있기 때문에 사용할 때마다 쿼리를 날리게 된다. 이 때 N + 1문제가 발생한다.(회원을 select하는 쿼리 실행(1)에 연관된 팀마다 N번 select 쿼리를 실행한다.)
하지만 페치 조인을 사용하면 지연로딩으로 설정 되어있다 해도 회원과 연관된 팀 엔티티는 프록시가 아닌 실제 엔티티가 반환된다.
컬렉션 페치 조인
List<Team> result = em.createQuery("select t from Team t join fetch t.members", Team.class)
.getResultList();
for (Team team : result) {
System.out.println("team = " + team.getName() + "|members = " + team.getMembers().size());
for (Member member : team.getMembers()) {
System.out.println(" -> member = " + member);
}
}일대다 관계를 조회할 때는 데이터가 훨씬 많이 조회될 수도 있다.
하이버네이트6 부터는 distinct 자동 적용된다.
페치 조인의 한계
페치 조인 대상에는 별칭을 줄 수 없다.
JPA 표준에서는 지원하지 않지만 하이버네이트에서는 지원한다. 별칭을 잘못 사용하면 연관된 데이터 수가 달라져서 데이터 무결성이 깨질 수 있으므로 조심해서 사용해야 한다.(가급적 사용하면 안 된다.)
둘 이상의 컬렉션은 페치 조인 할 수 없다.
컬렉션 * 컬렉션이 되기 때문에 카테시안 곱이 만들어진다.
컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
일대일, 다대일 같은 단일 값 연관 필드들은 페치 조인해도 페이징이 가능하다.
하이버네이트는 컬렉션을 페지 조인하고 페이징을 하면 경고 로그를 남기면서 메모리에서 페이징 처리를 한다.(매우 위험하다.)
객체 그래프 탐색 방향을 반대로 하거나
@BatchSize로 해결할 수도 있다.
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능 최적화를 할 수 있다. 페치 조인은 엔티티에 직접 적용하는 글로벌 로딩 전략(FetchType.LAZY)보다 우선한다. 글로벌 로딩 전략은 될 수 있으면 모두 지연 로딩을 사용하고 최적화가 필요하면 페치 조인을 적용하는 곳이 효과적이다.
모든 것을 페치 조인으로 해결할 수는 없다. 페치 조인은 객체 그래프를 유지할 때 사용하면 효과적이다.
여러 테이블을 조인해서 엔티티가 가진 모양이 아닌 전혀 다른 결과를 내야 한다면 억지로 페치 조인을 사용하기보다는 일반 조인을 사용하고 여러 테이블에서 필요한 필드들만 조회해서 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')엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다. Item 중에 Book, Movie를 조회한다.
treat
//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'자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. 부모 타입인 Item을 자식 타입인 Book으로 다룬다.
엔티티 직접 사용
기본 키 값
객체 인스턴스는 참조 값으로 식별하고 테이블 로우는 기본 키 값으로 식별한다. JPQL에서 엔티티 객체를 직접 사용하면 SQL에서는 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m //엔티티의 ID를 사용
select count(m) from Member m //엔티티를 직접 사용
//실행된 SQL, 둘 다 같다.
select count(m.id) as cnt from Member m엔티티를 파라미터로 직접 받아도 식별자 값(Id)을 직접 파라미터로 받는 것이랑 똑같다.
외래 키 값 : 외래 키 값도 기본 키 값과 똑같은 원리로 동작한다.
Named 쿼리
미리 정의해서 이름을 부여해두고 사용하는 JPQL이다. 정적 쿼리이다.
@Entity
@NamedQueries({
@NamedQuery(name = "Member.findByUsername", query = "select m from Member m where m.name = :name")
})
public class Member{
...
}
List<Member> result = em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("name", "회원1")
.getResultList();Named 쿼리는 애플리케이션 로딩 시점에 문법을 체크하고 미리 파싱해 둔다. 그래서 오류를 빨리 확인할 수 있고 사용하는 시점에는 파싱된 결과를 재사용한다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 DB의 조회 성능 최적화에도 도움이 된다.
(인텔리제이 옵션 중에 Add Named Query를 사용하면 편리하게 Named 쿼리를 만들어준다.)
스프링 데이터 JPA에서는 쿼리 메서드 위에 Named 쿼리를 정의할 수 있다.
벌크 연산
엔티티를 수정하려면 영속성 컨텍스트의 변경 감지 기능이나 병합을 사용하고 삭제하려면
em.remove()메서드를 사용한다. 하지만 이 방법으로 수백 개 이상의 엔티티를 하나씩 처리하기에는 시간이 너무 오래 걸린다. 이럴 때 여러 건을 한 번에 수정할 수 있는 벌크 연산을 사용하면 된다.
int resultCount = em.createQuery("update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount")
.setParameter("stockAmount", 10)
.executeUpdate();재고가 10개 미만인 모든 상품의 가격을 10% 상승 시켰다. 이때 벌크 연산을 수행하기 위해 executeUpdate()메서드를 사용한다. 반환값은 벌크 연산으로 영향받은 로우의 수다. 삭제도 같은 메서드를 사용한다.
벌크 연산은 영속성 컨텍스트를 무시하고 DB에 직접 쿼리하기 때문에 주의해야 한다.
영속성 컨텍스트를 무시한다는 것은 1차 캐시에 남아있는 엔티티는 그대로 있고 DB에만 반영한다는 것이다. 즉 벌크 연산 수행 후 데이터를 조회하면 1차 캐시는 그대로이기 때문에 수정 전 데이터가 조회되는 것이다.
그래서 벌크 연산을 가장 먼저 실행을 하거나 벌크 연산 수행 후 영속성 컨텍스트를 초기화해 주어야 한다.
Last updated