개발 일기

[섹션 10] 객체지향 쿼리 언어(JPQL) - 1 본문

Back-End/자바 ORM 표준 JPA 프로그래밍

[섹션 10] 객체지향 쿼리 언어(JPQL) - 1

개발 일기장 주인 2024. 4. 10. 16:18

JPA는 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL, JDBC API 등 다양한 쿼리를 지원한다.

그 중 JPQL에 대해 알아보자

JPQL(Java Persistence Query Language)

JPA를 사용하면 엔티티 객체를 중심으로 개발하는데 문제는 검색 쿼리이다. 

검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색하는데 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능하다.

그래서 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요!

JPA는 SQL을 추상화하여 엔티티 객체를 대상으로 검색하는 JPQL이라는 객체 지향 쿼리 언어 제공
이때 SQL을 추상화한 것이기 때문에 특정 데이터베이스 SQL에 의존하 지 않는다.

JPQL 기본 문법

ex) select m from Member as m where m.age > 18

  • 엔티티와 속성은 대소문자 구분O (Member, age)
  • JPQL 키워드는 대소문자 구분X (SELECT, FROM, where) 
  • 엔티티 이름 사용, 테이블 이름이 아님(Member)
  • 별칭은 필수(m) (as는 생략가능)
  • 그룹 - GROUP BY / HAVING 
  • 정렬 - ORDER BY
select
    COUNT(m), //회원수
    SUM(m.age), //나이 합
    AVG(m.age), //평균 나이 MAX(m.age), //최대 나이 MIN(m.age) //최소 나이
from Member m

 

TypedQuery & Query

// TypeQuery: 반환 타입이 명확할 때 사용 
TypedQuery<Member> query =
	em.createQuery("SELECT m FROM Member m", Member.class);

// Query: 반환 타입이 명확하지 않을 때 사용
Query query =
	em.createQuery("SELECT m.username, m.age from Member m");

 

 

결과 조회 API

  • query.getResultList()
    : 결과가 하나 이상일 때, 리스트 반환 결과가 없으면 빈 리스트 반환
  • query.getSingleResult()
    : 결과가 정확히 하나일떄,
    단일 객체 반환 결과가 없으면: javax.persistence.NoResultException
    둘 이상이면: javax.persistence.NonUniqueResultException
    Spring Data JPA로 예외를 반환하지 않는걸로 처리해줌 이 겨우에는 try-catch필요

 

파라미터 바인딘 - 이름 기준, 위치 기준

// 이름 기준
SELECT m FROM Member m where m.username=:username query.setParameter("username", usernameParam);

// 위치 기준
SELECT m FROM Member m where m.username=?1 query.setParameter(1, usernameParam);

 

프로젝션
SELECT 절에 조회할 대상을 지정하는 것

엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)

DISTINCT로 중복 제거한다.

// 엔티티 프로젝션
SELECT m FROM Member m 

// 엔티티 프로젝션 -> 사실 이것은 join이 직관적으로 보이게 join문으로 써주는 것이 유리하다고 한다.
SELECT m.team FROM Member m 

// 임베디드 타입 프로젝션 
SELECT m.address FROM Member m
엔티티 프로젝션 시에 여러개의 엔티티들이 모두 영속성 컨텍스트에서 관리된다.

 

// 스칼라 타입 프로젝션
SELECT m.username, m.age FROM Member m

해당 쿼리와 같이 여러 값(String, Int)으로 쿼리되는 경우에는 어떻게 될까?

  1. 쿼리 타입
  2. Object[] 타입으로 조회
  3. new 명령어로 조회
    : 단순 값을 DTO로 조회
    SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
    이 경우에 패키지 명을 포함한 전체 클래스명 입력
    순서와 타입이 일치하는 생성자 필요 
    -> 현재는 문자열이지만 QueryDSL로 극복 가능

페이징 API

Truss 프로젝트를 진행하면서 페이징을 하는 과정에서 쿼리를 일일이 다 짜기 정말 귀찮다는 생각을 했었는데 이것은 정말 편리했다.

아래의 페이징 코드를 MYSQL, ORACLE 방언으로 처리하게 되면 정말 복잡하고 귀찮다.

// 페이징 쿼리
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();

 

조인

// 내부 조인:
SELECT m FROM Member m [INNER] JOIN m.team t

// 외부 조인:
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t

// 세타 조인:
select count(m) from Member m, Team t where m.username = t.name

 

JPA 2.1부터 ON절을 활용한 조인이 가능한데

  1. 조인 대상 필터링
    예) 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
    JPQL: SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
    SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.TEAM_ID=t.id and t.name='A'
  2. 연관관계 없는 엔티티 외부 조인
    예) 회원의 이름과 팀의 이름이 같은 대상 외부 조인
    JPQL: SELECT m, t FROM Member m LEFT JOIN Team t on m.username = t.name
    SQL: SELECT m.*, t.* FROM Member m LEFT JOIN Team t ON m.username = t.name
     

서브 쿼리

// 나이가 평균보다 많은 회원
select m from Member m 
where m.age > (select avg(m2.age) from Member m2)

// 한 건이라도 주문한 고객
select m from Member m
where (select count(o) from Order o where m = o.member) > 0
  • [NOT] EXISTS (subquery): 서브쿼리에 결과가 존재하면 참
  • {ALL | ANY | SOME} (subquery)
    ALL 모두 만족하면 참
    ANY, SOME은 같은 의미로 조건을 하나라도 만족하면 참
  • [NOT] IN (subquery): 서브쿼리의 결과 중 하나라도 같은 것이 있으면

  • JPA에서 WHERE, HAVING 절에서 서브 쿼리 지원
  • SELECT는 하이버네이트에서 지원, 하이버네이트6부터 FROM도 지원

 

JPQL 타입 표현

  • 문자: ‘HELLO’, ‘She’’s’
  • 숫자: 10L(Long), 10D(Double), 10F(Float)
  • Boolean: TRUE, FALSE
  • ENUM: jpabook.MemberType.Admin (패키지명 포함)
  • 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)

 

JPQL 기타

  • SQL과 문법이 같은 식 (표준 SQL은 거의 다 지원)
  • EXISTS, IN
  • AND, OR, NOT
  • =, >, >=, <, <=, <> BETWEEN, LIKE, IS NULL

조건식 - CASE 식

-- 기본 CASE 식
select
	case when m.age <= 10 then '학생요금' 
    	 when m.age >= 60 then '경로요금'
         else '일반요금'
	end
from Member m

-- 단순 CASE 식
select
	case t.name
		when '팀A' then '인센티브110%' 
        when '팀B' then '인센티브120%'
        else '인센티브105%'
	end
from Team t

 

COALESCE & NULLIF

COALESCE
: 하나씩 조회해서 null이 아니면 반환,

사용자 이름이 없으면 이름 없는 회원을 반환

select coalesce(m.username,'이름 없는 회원') from Member m

NULLIF

: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환,

사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환 

select NULLIF(m.username, '관리자') from Member m

 

JPQL 기본 함수
CONCAT SUBSTRING TRIM LOWER UPPER LENGTH LOCATE ABS SQRT MOD SIZE INDEX(JPA 용도)

이때, 사용자 정의 함수도 직접 정의하여 사용가능하다.

ex) select function('group_concat', i.name) from Item i

사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다