자바 ORM 표준 JPA 프로그래밍 - 기본편 : JPQL 문법 기본

jkky98·2024년 10월 7일
0

Spring

목록 보기
53/77

JPQL이란

JPQL은 JPA의 한계를 극복하기 위해 사용된다. 복잡한 쿼리 작성을 가능하게 하며 조인 및 집계 함수를 활용할 수 있어, 데이터의 다양한 집합 및 분석을 효과적으로 수행할 수 있다. JPQL은 SQL과 유사한 문법을 사용하여 직관적인 쿼리를 작성할 수 있도록 해주고, 이를 통해 팀원 간(DBA)의 소통이 원활해질 수 있다. 또한 특정 데이터베이스 기능을 활용하거나 성능 최적화를 통해 효율적으로 데이터를 쿼리할 수 있는 장점을 제공한다.

JPQL 문법

엔티티와 속성은 대소문자 구분이 존재하며, JPQL 키워드(select, from...)는 대소문자 구분이 없다.

엔티티 이름을 사용하며 테이블 이름은 사용하지 않는다.

alias(별칭)사용은 필수이다. 다만 as는 생략 가능하다.

TypeQuery, Query

TypeQuery는 반환 타입이 명확할 때 사용하고 Query는 반환 타입이 명확하지 않을 때 사용한다.

TypedQuery<Member> query = 
	em.createQuery("SELECT m FROM Member m", Member.class);
    
Query query = 
	em.createQuery("SELECT m.username, m.age FROM Member m", Member.class);

위의 예시에서 첫번째 query의 경우 반환 타입이 Member로 정확하다. 하지만 아래의 query 코드의 경우 String username과 int age가 같이 존재하므로 TypeQuery 사용이 불가능하다.

query.getResultList()

결과를 리스트로 반환하는 것인데, 결과가 없다면 빈 리스트로 반환한다.

query.getSingleList()

결과가 정확히 하나여야 한다. 만약 둘 이상이거나 결과가 없으면 각각의 예외가 발생한다.

query.setParameter()

em.createQuery("select m from Member m where m.username=:username")
		.setParameter("username", usernameParam);

동적 파라미터 할당은 위와 같이 가능하다.

em.createQuery("select m from Member m where m.username=?1")
		.setParameter(1, usernameParam);

위의 코드처럼 순서를 표기하는 숫자를 주어 매핑할 수 있지만 이 방식은 추천하지 않는다. 수정시 숫자가 밀릴 수 있기 때문에 변수로 매핑하는 방식을 사용하도록 하자.

프로젝션

프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것으로 select 다음에 위치하는 것에 따라 종류가 달라진다.

• SELECT m FROM Member m -> 엔티티 프로젝션
• SELECT m.team FROM Member m -> 엔티티 프로젝션
• SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
• SELECT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션

이 경우 Member가 영속성 컨텍스트에 존재하지 않아 엔티티 조회를 DB에서 수행한 경우라면 Member 엔티티가 영속성 컨텍스트에 올라간다.

Member의 연관관계 엔티티인 Team을 가져오는 경우 Team까지 영속성 컨텍스트에 올라간다고 보면 된다.

여러 값 조회

em.createQuery("SELECT m.username, m.age FROM Member m")

Member 엔티티의 username, age만 조회를 원할 경우 Query 타입으로 조회할 수 있다고 앞서 설명했다.

.getResultList()를 활용할 경우 Object[]로 반환받는데 이렇게 반환받아 사용하는 방식보다는

결과를 받기 위한 DTO 클래스를 만들어 다음과 같이 사용하는 방식을 권장한다.

em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM
Member m")
.getResultList()

이러한 new 방식으로 조회할 경우 DTO 객체로 바로 꺼낼 수 있어 Object[]를 풀어 사용하는 과정(리스트에서 빼내고 다운캐스팅)을 생략할 수 있다.

Paging

SELECT *
FROM (
    SELECT 
        m.*, 
        ROWNUM AS rn
    FROM (
        SELECT 
            m.*
        FROM 
            Member m
        WHERE 
            m.status = 'ACTIVE'
        ORDER BY 
            m.registration_date DESC  -- 원하는 정렬 기준
    ) 
    WHERE ROWNUM <= :endRow
)
WHERE rn > :startRow;

위는 예전 Oracle DB의 페이징을 처리하는 쿼리다.(현재 Oracle DB는 조금 간단한 편이지만)

이렇게 복잡할 수 있는 페이징 쿼리를 JPA는 매우 단순화해서 처리가 가능하다.

JPA는 단 두가지 API로 페이징을 가능하게 한다.

  • setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
  • setMaxResults(int maxResult) : 조회할 데이터 수
//페이징 쿼리
String jpql = "select m from Member m order by m.name desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
.getResultList();

역시 query에 체이닝 형식으로 두 메서드만을 사용해서 페이징 처리가 가능하다.

Join

조인은 가장 많이 사용하는 내부조인(inner join), 외부조인(left, right), Full조인, 세타 조인을 jpql로 구현 가능하다.

먼저 위의 글에서 문제점을 하나 파악할 수 있는데

SELECT m.team FROM Member m -> 엔티티 프로젝션

프로젝션 설명에서 Member로부터 연관관계인 Team 엔티티를 가져오기 위해 위와 같이 코드를 짰다. 위의 select 쿼리는 team에 대해 join 쿼리가 나가게 된다.

jpql을 잘 이해하고 있다면 위의 코드가 join 쿼리가 나가는 것을 예상할 수 있지만 그렇지 않을 경우를 고려해서 다음과 같이 member join team jpql 쿼리로 표현해놓는 것이 범용적으로 좋은 가독성을 제공한다.

(jpa개발자만 이해가 편한) "SELECT m.team FROM Member m"

(sql을 아는 모두가 이해가 편한) "SELECT m.team FROM Member m JOIN m.team t"

그냥 Join 키워드만 사용할 경우 Inner Join으로 적용된다.

Outer Join의 경우 LEFT (OUTER) JOIN 키워드를 사용한다.(Right, Left)

세타 조인

select count(m) from Member m, Team t where m.username= t.name

흔히 "막 조인"이라고 불리는 조인이다. 즉, 세타 조인은 관계형 데이터베이스에서 두 테이블 간의 모든 조건을 통해 결합할 수 있는 조인을 의미한다.

ON

조인 이전 해당 테이블에 조건을 주는 필터링을 ON절을 통해 가능한데, JPQL또한 이를 지원한다.

"SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'"

서브 쿼리

JPA 서브 쿼리는 일반적인 sql과 달리 WHERE, HAVING 절에서만 사용이 가능한 한계가 존재해서 다른 절에서 사용시 쿼리를 두 번 날리거나 다른 방식으로 최적화를 진행해야했으나 버전업이 되면서 이들을 지원하는 방향으로 업데이트 되고 있다.

from절의 서브쿼리는 Hibernate6(2022.06)부터 지원하고있으며, select 절의 서브쿼리의 경우 Hibernate에서는 가능하다.

select m from Member m
where exists (select t from m.team t where t.name = ‘팀A')
-> "팀A라는 이름을 가진 팀이 있는 모든 회원(Member)을 선택한다."

select o from Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
-> "Order의 orderAmount가 Product의 stockAmount보다 큰 모든 Order를 선택한다."

select m from Member m
where m.team = ANY (select t from Team t)
-> "Member의 team이 Team에 하나라도 존재하면 선택한다."

[NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
{ALL | ANY | SOME} (subquery)
ALL 모두 만족하면 참
ANY, SOME: 같은 의미, 조건을 하나라도 만족하면 참
[NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참

profile
자바집사의 거북이 수련법

0개의 댓글