소개
JPA는 다양한 쿼리 방법을 지원하는데 그 예시로는 JPQL, JPA Criteria, QueryDSL, 네이티브 SQL 등의 방식이 있다. 앞의 3개는 자바코드로 JPQL을 짜주는 클래스의 집합이라고 이해하면 된다. 이외에도 JPA와 함께 JDBC API 직접사용, MyBatis, SpringJdbcTemplate 함께 사용 하는 방법 등이 있는데 Hibernate의 창시자 개빈 킹의 말, `모든 문제를 해결하기 위해 제작된 것이 아니다.` 처럼 필요시 ORM이 가능한 다양한 방법을 사용하면 된다.
오늘은 이 중 JPQL을 사용하는 방법을 정리해보려고 한다.
JPQL
JPQL의 가장 단순한 조회 방법은 EntityManager의 find() 메서드를 통한 방법이다. 이를 통해 받아온 객체를 그래프 탐색을 통해 데이터를 얻어볼 수 있는데 만약 복잡한 조건식이 들어간다면 어떻게 해야할까?
예를 들어 "나이가 18살 이상인 회원을 모두 검색하고 싶다면?"과 같은 조건 말이다.
JPA를 사용하면 데이터베이스 테이블 중심이 아닌 엔티티 객체를 중심으로 개발하게 된다. 이 때 위의 조건식처럼 검색 쿼리를 받아보고 싶을 때 복잡한 문제가 생기게 되는데 결국 JPA는 테이블 대상이 아닌 엔티티 객체를 대상으로 검색이 필요하다. 하지만 객체에 모든 DB 데이터를 받아서 검색하는 것은 불가능하기 때문에 결국은 DB에서 쿼리를 한 데이터를 받아와야만 한다. 바로 이 때 필요한 것이 우리가 필요하나 조건이 포함된 SQL이 필요하다.
이 때 JPA는 SQL을 추상화한 JPQL은 객체 지향 쿼리 언어를 제공한다. 기본적인 SQL 문법인 SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN을 지원한다.
select m From Member m where m.age > 18;
기본적인 SQL의 형태와 같지만 from 절에 테이블이 아닌 엔티티 객체가 포함되어 객체 중심으로 작성된다는 것을 확인할 수 있다.
JPQL은 SQL을 추상화하여 특정 데이터베이스 SQL에 의존하지 않는다는 특징을 가지고 있다. 따라서 데이터베이스에 맞는 방언 설정을 하여 사용이 가능하다.
다만, JPQL이 가지는 단점으로는 위의 JPQL은 단순 String이기 때문에 여러 필드에 대한 동적쿼리를 작성하기 어렵다는 점이다. 이를 해결하기 위한 방법으로는 JPA 표준으로 제공하는 Criteria나 QueryDSL을 사용하면 해결이 가능하다.
Criteria VS QueryDSL
Criteria는 JPQL 빌더 역할을 하여 문자가 아닌 자바코드로 JPQL을 작성할 수 있다. 따라서 동적 쿼리를 작성할 때도 JPQL에서는 String을 더하고 빼는 과정에서 버그가 날 확률이 비교적 높지만, Criteria에서는 오타시 컴파일 혹은 IDE 단계에서 버그를 잡을 수 있는 장점이 있다. 하지만 치명적인 단점으로는 너무 복잡하고 실용성이 없다.
QueryDSL는 OpenSource 라이브러리로 동적쿼리를 더 쉽게 만들어준다. QueryDSL을 사용하려면 기본적인 세팅이 필요하지만 그만큼 강력한 기능을 제공하기 때문에 유용하다. 자바코드로 작성하여 컴파일 시점에서 오류를 찾을 수 있는 장점, 동적쿼리 작성이 편리하다는 장점 등 Criteria의 장점을 포함하면서도 단순하고 쉽기 때문에 실무에서 사용이 권장된다.
네이티브 SQL
네이티브 SQL은 JPA가 제공하는 SQL을 직접 사용하는 기능이다. 주로 JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능을 사용할 때 사용한다. 예를들어 오라클의 CONNECT BY나 특정 데이터베이스만 사용하는 SQL 힌트 등을 말한다.
em.createNativeQuery("select MEMBER_ID, city, street, USERNAME from MEMBER").getResultList();
이외 다른 방법 - JDBC API / MyBatis, SpringJdbcTemplate...
JPA와 함께 JDBC API 직접 사용하거나 MyBatis, SpringJdbcTemplate 함께 사용할 수 있다.
다만 이 경우 JPA 기술을 사용하는 것이 아니므로 DB를 사용하기 위해 적절한 시점에 영속성 컨텍스트를 강제로 플러시 해주는 작업이 필요하다.
기본 문법과 쿼리 API
JPQL -> SQL
JPQL 문법
위에서 본 쿼리문과 함께 JPQL의 문법 특징을 알아보자.
select m From Member (as) m where m.age > 18;
- 엔티티와 속성은 대소문자를 구분한다 - Member, age
- JPQL 키워드는 대소문자를 구분하지 않는다 - SELECT, from, WHERE
- 테이블 이름이 아닌 엔티티 이름을 사용한다 - Member
- 별칭은 필수이다 (as 생략가능) - Member m
- 집합과 정렬
- 기본적인 COUNT(m), SUM(m.age), AVG(m.age), MAX(m.age), MIN(m.age) 등을 제공한다.
- GROUP BY, HAVING / ORDER BY 제공한다.
TypeQuery, Query
TypeQuery은 반환 타입이 명확할 때 사용하며, Query는 반환 타입이 명확하지 않을 때 사용한다.
결과 조회 API
- query.getResultList()
- 결과가 하나 이상일 때 사용, 리스트 반환 결과가 없으면 빈 리스트 반환한다.
- NullPointException에서 해방된다.
- 결과가 하나 이상일 때 사용, 리스트 반환 결과가 없으면 빈 리스트 반환한다.
- query.getSingleResult()
- 결과가 정확히 하나일 때 사용, 결과가 없거나 둘 이상이면 다음 예외를 반환한다.
- 단일 객체 없을시: javax.persistence.NoResultException
- 둘 이상이면: javax.persistence.NonUniqueResultException
- 다만, JPA에서 try-catch를 사용해서 버전에 따라 null 혹은 Optional을 반환하도록 작성되어있다.
- 결과가 정확히 하나일 때 사용, 결과가 없거나 둘 이상이면 다음 예외를 반환한다.
파라미터 바인딩 방법
이름 기준일 때는 `:` 콜론을 사용하여 바인딩하고, 위치 기준일 때는 ? 물음표를 사용하여 바인딩한다.
select m From Member (as) m where m.username = :username
select m From Member (as) m where m.username = ?1
JPA도 MyBatis처럼 위치 기준(1부터시작) 혹은 이름 기준으로 파라미터 세팅을 할 수 있다.
query.setParameter("username", usernameParam);
query.setParameter(1, usernameParam);
다만, 엔티티에서 enum을 숫자로 사용할 때처럼 파라미터 바인딩도 숫자로 사용될 시 밀리면 모든 파라미터가 틀어지므로 명시적인 이름을 기준으로 사용하는 것이 좋다.
프로젝션(SELECT)
프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것을 말한다. 프로젝션에서 대상으로 삼을수 있는 것은 엔티티(m), 임베디드 타입, 스칼라 타입(m.age - 숫자, 문자 등 기본 데이터 타입) 등이 가능하다.
select m From Member (as) m ---> 엔티티 프로젝션
select m.team From Member (as) m ---> 엔티티 프로젝션
select m.address From Member (as) m ---> 임베디드 타입 프로젝션
select m.age From Member (as) m ---> 스칼라 타입 프로젝션
또한, 프로젝션 대상 앞에 DISTINCT를 넣음으로서 중복 제거가 가능하다.
중요한 점은 프로젝션 대상에 작성되어 받아온 객체들은 영속성 컨텍스트에서 관리가 된다는 점이다.
엔티티 타입 프로젝션
두번째 team의 경우에는 조회시 join SQL이 나가는데 복잡한 실무에서는 예측이 힘든 코드로 작성하는 것이 좋지 않으므로 직접 join할 엔티티를 명시하여 다음과 같이 개선하는 것이 좋다.
select m.team From Member (as) m
select t From Member (as) m join m.team t ---> 조인 대상 명시
위 jpql문에서 아래와 같이 명시하는 것이 운영 측면에서 쿼리를 튜닝할 때 더욱 명시적이고 쉽게 이해할 수 있는 jpql문이 될 것이다.
스칼라 타입 프로젝션
- Query 타입으로 조회
- Object[] 타입으로 조회
- new 명령어로 조회
- 단순값을 DTO로 바로 조회
- 패키지명을 포함한 전체 클래스명 입력
- 순서와 타입이 일치하는 생성자 필요
SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m
위와 같이 JPQL에 DTO 생성자를 사용할 수 있다. 다만, 패키지명을 모두 적어야한다는 문제점이 있다.
페이징
JPA는 다른 ORM 기술이 제공하는 복잡한 페이징 처리 기법과는 달리 페이징을 다음 두 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();
조인
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 ---세타 조인:
내부조인 - team이 없을시 결과값이 없음
외부조인 - team이 없을시 team은 null인 상태로 조인된 member를 가져옴
세타조인 - m * t
Database - 관계 대수 - part 2
Database - 관계 대수 - part 2
velog.io
조인 - ON 절
ON절을 활용한 조인을 JPA 2.1부터 사용할 수 있다. 주로 1. 조인 대상 필터링을 하거나 2. 연관관계 없는 엔티티 외부 조인을 할 때 사용한다. 2번의 경우에는 Hibernate 5.1 부터 지원한다.
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
서브 쿼리
JPA는 일반적인 SQL에서 사용가능한 서브쿼리를 사용할 수 있다.
서브 쿼리 예시
나이가 평균보다 많은 회원
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): 서브쿼리의 결과 중 하나라도 같은 것이 있으면 참
서브쿼리 함수 예시
팀A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = '팀A')
전체 상품 각각의 재고보다 주문량이 많은 주문들
select o from Order o
where o.orderAmount > ALL (select p.stockAmount from Product p)
어떤 팀이든 팀에 소속된 회원
select m from Member m
where m.team = ANY (select t from Team t)
JPA 서브쿼리 한계점
JPA는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능하고 구현체로 Hibernate를 사용하는 경우에는 SELECT 절에서도 사용 가능하다고 한다. FROM 절의 경우에는 Hibernate 6부터 지원한다.
JPQL 타입 표현과 기타식
- 문자 - 작은 따옴표 안에 표기 : 'word', 'She''s'
- 숫자 : 10L(Long), 10D(Double), 10F(Float)
- Boolean : TRUE, FALSE
- ENUM - 패키지명을 포함하여 표기 : com.package.MemberType.Admin
- 엔티티 타입 - 상속 관계(dtype)에서 사용 : TYPE(m) = Member
이외 SQL과 문법이 같은 식들..
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
조건식(CASE 등등)
- 기본 CASE 식
select
case
when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금' end
else '일반요금'
from Member m
- 단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
- COALESCE: 하나씩 조회해서 null이 아니면 반환
사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
- NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
JPQL 함수
- JPQL 기본 함수
- CONCAT - Hibernate ('a' || 'b') 사용 가능
- SUBSTRING
- TRIM - ltrim rtrim 사용가능
- LOWER, UPPER
- LENGTH
- LOCATE - 찾고자하는 문자의 위치
- ABS, SQRT, MOD
- SIZE, INDEX(JPA 용도)
- size(t.members) -> collection의 크기 반환
- INDEX - `@OrderColumn` 사용시 가능(비추천)
- 사용자 정의 함수
- 하이버네이트는 사용 전 방언에 추가해야 한다. 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
- 다음은 group_concat이라는 함수를 사용하여 i.name을 로직 처리하는 예제이다.
select function('group_concat', i.name) from Item i
'노트 정리 > 자바 ORM 표준 JPA 프로그래밍' 카테고리의 다른 글
[JPA이론] 11. 객체지향 쿼리 언어2 - 중급 문법 (0) | 2022.10.23 |
---|---|
[JPA이론] 9.값 타입 (0) | 2022.10.20 |
[JPA이론] 8. 프록시와 연관관계 관리 (0) | 2022.10.19 |
[JPA이론] 7. 고급매핑 (0) | 2022.10.14 |
[JPA이론] 6. 다양한 연관관계 매핑 (0) | 2022.10.13 |