JPQL
JPQL은 JPA에서 사용하는 쿼리로, 테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이다.
테이블 member_tb가 있고 해당 테이블과 매핑되는 Member 엔티티가 있다고 할 때,
회원 데이터를 전체 조회하는 SQL 쿼리와 JPQL을 표현하자면, 다음과 같다.
SQL
SELECT * FROM member_tb
JPQL
select m from Member as m
이처럼 JPQL은 객체인 Member 엔티티를 대상으로 하며, 별칭은 필수이다.
[ 조회 ]
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 하는데 프로젝션 대상에는 엔티티, 임베디드 타입, 스칼라 타입이 있다.
SELECT (프로젝션 대상) FROM (엔티티)
쿼리를 실행하는 쿼리 객체로는 TypedQuery와 Query가 있다.
TypedQuery는 조회 대상이 명확할 때 사용되는데, 조회 대상이 명확하지 않은 Query는 아래 코드처럼 형변환을 해줘야 해 번거로움이 있다.
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List<Object[]> resultList = query.getResultList();
for (Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
NEW 명령어
형변환을 해야 하는 번거로움을 덜어주는 친구가 바로 NEW 명령어이다.
조회 대상이 명확하지 않을 때 조회 대상을 필드로 가지는, 클래스를 정의해 조회 대상으로 지정해주면 된다.
그러면, 새로 정의한 UserDTO 클래스의 생성자에 JPQL 조회 결과를 넘겨주면서 객체 변환 작업을 하지 않아도 되는 것이다.
TypedQuery<UserDTO> query = em.createQuery("SELECT new japbook.jpql.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
[ 페이징 ]
JPA는 페이징을 다음 두 API로 추상화했다.
1. setFirstResult(int startPosition): 조회 시작 위치
2. setMaxResults(int maxResult): 조회할 데이터 수
아래 코드는 11번째부터 총 20건의 데이터를 조회하는 (11~30번 데이터를 조회하는) 코드이다.
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class);
query.setFirstResult(10);
query.setMaxresults(20);
query.getResultList();
여기서 중요한 JPA의 특징 중 하나! 두 API는 모든 데이터베이스에 사용할 수 있다. JPA는 특정 데이터베이스에 의존하지 않으므로 데이터베이스 방언에 따라 알맞는 SQL로 변환해주기 때문이다.
[ 조인 ]
JPQL은 SQL과 다르게 JOIN 명령어 다음에 조인할 객체의 연관 필드를 사용한다.
SELECT m FROM Member m INNER JOIN Team t # NO
SELECT m FROM Member m INNER JOIN m.team t # OK
위 JPQL로 생성되는 SQL
[ 페치 조인 ]
SQL의 조인과는 다르며, JPQL에서 성능 최적화를 위해 제공하는 기능이다.
페치 조인을 사용하면 연관된 엔티티나 컬렉션을 한 번에 같이 조회한다.
주의점은 일반적인 JPQL 조인과 다르게 m.team 다음에 별칭이 없는데 페치 조인은 별칭을 사용할 수 없다.
SELECT m FROM Member m JOIN FETCH m.team
위 JPQL로 실행되는 SQL
조인과 페치 조인이 실행하는 SQL이 다른 것을 볼 수 있다!
페치 조인은 회원과 연관된 팀도 함께 조회된 것을 확인할 수 있다. 회원과 팀을 지연 로딩으로 설정했다고 해도, 페치 조인을 사용하면 지연 로딩이 일어나지 않는다.
조인은 프로젝션에 지정한 대상만 조회되기 때문에(위에서는 회원만) 지연 로딩일 때 팀을 member.getTeam() 과 같이 조회하면 그때서야 팀을 조회하는 쿼리가 한번 더 실행된다.
이처럼 페치 조인을 사용하면 SQL 호출 횟수를 줄여 성능을 최적화할 수 있기 때문에, 같이 조회할 경우가 많을 경우에는 페치 조인을 사용하면 좋다.
[ 페치 조인 주의점 ]
페치 조인 대상은 별칭을 줄 수 없기 때문에 SELECT, WHERE 절, 서브쿼리에 페치 조인 대상을 사용할 수 없다.
페이징 API(setFirstResult, setMaxResults)는 SQL의 LIMIT과 OFFSET을 사용해 데이터베이스 수준에서 페이징 처리 후 메모리로 가져온다. 그러나 컬렉션을 페치 조인할 경우, 원하는 결과를 얻지 못할 수 있다. (개발팀, 디자인팀, 기획팀이 있고 각 인원이 5명일 때 2개로 페이징한다면.. 개발팀, 디자인팀이 나오는 것이 아니라 개발팀 인원 5명 중 2명만 나오는 결과를 얻는다.) 따라서 JPA는 SQL에서 페이징 처리를 하는 것이 아니라 모든 데이터를 메모리로 가져온 후 메모리에서 페이징 처리한다. 따라서 데이터가 많으면 성능 이슈나 메모리 초과의 위험이 있기 때문에 사용 안하는 것이 좋다.
[ 묵시적 조인 ]
아래 JPQL 처럼 JOIN 명령어를 명시하지 않고 연관 엔티티를 조회하면 묵시적으로 조인을 수행한다.
select o.member.team from Order o; # JPQL
# 실행되는 SQL
select t.* from Orders o
inner join Member m on o.member_id=m.id
inner join Team t on m.team_id=t.id
inner join Product p on o.product_id=p.id
where p.name='productA' and o.city='SEOUL'
팀과 회원처럼 일대다일 경우, 연관 엔티티의 필드까지는 가져올 수 없다. 가져오려면 join을 명시적으로 적어주고 별칭을 통해 가져와야 한다.
select t.members from Team t // OK
select t.members.name from Team t // NO
select m.name from Team t join t.members m // OK
묵시적 조인은 일어나는 상황을 한눈에 파악하기 어렵기 때문에 성능이 중요하면 분석하기 쉽도록 명시적 조인이 좋다.
[ 서브쿼리 ]
서브 쿼리를 WHERE, HAVING 절에서는 사용할 수 있지만 SELECT, FROM 절에서는 사용할 수 없다.