객체 지향 쿼리 언어 소개
EntityManager.find()나 객체 그래프 탐색은 가방 단순한 검색 방법이다. 하지만 이 기능만으로는 애플리케이션을 개발하기 어렵다. 예를 들어, 나이가 18살 이상인 회원을 모두 검색하고 싶자면 좀 더 현실적이고 복잡한 검색 방법이 필요하다. 그렇다고 해서 모든 회원 엔티티를 메모리에 올려두고 검색하는 것은 현실성이 없다. 결국 데이터는 데이터베이스에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 엔티티 객체를 대상으로 하는 방법이 필요하다.
JPQL은 이러한 문제를 해결하기 위해서 만들어졌다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체 지향 쿼리이다. JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
JPA는 JPQL 뿐만 아니라 다양한 검색 방법을 제공한다.
- JPQL
- Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다
다음은 JPA가 공식 지원하는 기능은 아니지만, 알아둘 가치가 있다
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크다
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하면 JDBC를 직접 사용할 수 있다.
JPQL 소개
JPQL은 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다. SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. JPQL을 한마디로 정의하면 객체 지향 SQL이다.
// 검색
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class) .getResultList();
// 실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
Criteria 소개
Criteria는 JPQL을 생성하는 빌더 클래스다. Criteria의 장점은 문자가 아닌 query.select(m).where(..)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
- 컴파일 시점에 오류를 발견할 수 있다
- IDE를 사용하면 코드 자동완성을 지원한다
- 동적 쿼리를 작성하기 편하다
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
//쿼리 생성 CriteriaQuery<Member> cq =
query.select(m).where(cb.equal(m.get("username"), “kim”));
List<Member> resultList = em.createQuery(cq).getResultList();
Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 것은 물론이고, Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다
QueryDSL 소개
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 뿐만 아니라 동적 쿼리를 작성하기 편하다.
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
네이티브 SQL 소개
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라고 한다. JPQL을 사용해도 가끔씩 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있는데 그 때 사용한다.
String sql =
“SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList =
em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
JDBC나 마이바티슬르 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다. JDBC를 직접 사용하든 마이바티스 같은 SQL 매퍼와 사용하든 모두 JPA를 우회해서 데이터베이스에 접근한다. 문제는 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다는 점이다.
이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
JPQL(Java Persistence Query Language)
JPQL 소개
- JPQL은 객체 지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라, 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
JPQL 문법
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE문을 사용할 수 있다. 엔티티를 저장할 때 EntityManager.persist() 메소드를 사용하면 되기 때문에 INSERT문은 따로 존재하지 않는다
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분을 한다 (객체에 있는 이름 그대로 작성해야 함 : Member, age)
- JPQL 키워드는 대소문자 구분 하지 않는다 (SELECT, FROM, where)
- JPQL에서 사용한 Member는 테이블 이름이 아니라, 엔티티의 이름이다.
엔티티 명을 지정하지 않으면 클래스명을 기본 값으로 사용한다. - 별칭은 필수이다. (m) (단, 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");
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환 - query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환
- 결과가 없으면 : javax.persistence.NoResultException
- 결과가 둘 이상이면 : javax.persistence.NonUniqueResultException
파라미터 바인딩
- 이름 기준 파라미터
파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 :를 사용한다.
SELECT m FROM Member m where m.username=:username
query.setParameter("username", usernameParam);
- 위치 기준 파라미터
? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작된다.
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
프로젝션
프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것이다. [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
프로젝션 대상
- 엔티티 프로젝션
이렇게 조회한 엔티티는 영속성 컨택스트에서 관리한다.
SELECT m FROM Member m; // 회원
SELECT m.team FROM Member m; // 팀
- 임베디드 타입 프로젝션
임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리하지 않는다.
SELECT m.address FROM Member m;
- 스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다. 중복 데이터를 제거하려면 DISTINCT를 사용한다.
SELECT m.username, m.age FROM Member m;
SELECT DISTINCT username FROM Member m;
프로젝션 여러 값 조회
엔티티를 대상으로 조회하면 편하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 프로젝션에 여러 값을 선택하면 TypeQuery 대신 Query를 사용해야 한다.
1. Query 타입으로 조회
List resultList = em
.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
2. Object 타입으로 조회
List<Object[]> resultList = em
.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result = resultList.get(0);
3. new 명령어로 조회
List<MemberDTO> resultList = em
.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO result = resultList.get(0);
페이징 API
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();
조인
조인 종류
- 내부 조인
내부 조인은 INNER JOIN을 사용한다. 참고로 INNER는 생략 가능하다
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
- 외부 조인
OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타 조인은 내부 조인만 지원한다.
select count(m) from Member m, Team t where m.username = t.name
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
서브 쿼리 지원 함수
- EXISTS
- 문법 : [NOT] EXISTS (subquery)
- 설명 : 서브 쿼리에 결과가 존재하면 참이다. NOT은 반대 - {ALL / ANY / SOME}
- 문법 : {ALL / ANY / SOME} (subquery)
- 설명 : 비교 연산자와 같이 사용한다.
-> ALL : 조건을 모두 만족하면 참이다
-> ANY 혹은 SOME : 둘은 같은 의미다. 조건을 하나라도 만족하면 참 - IN
- 문법 : [NOT] IN (subquery)
- 설명 : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브 쿼리가 아닌 곳에서도 사용한다.
팀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는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능하다
- SELECT 절도 가능하다 (하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다 (조인으로 풀 수 있으면 풀어서 해결)
JPQL 타입 표현
종류 | 설명 | 예제 |
문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개('') 사용 |
'HELLO' 'She''s' |
숫자 | L(Long 타입 지정) D(Double 타입 지정) F(Float 타입 지정) |
10L 10D 10F |
날짜 | DATE {d 'yyyy-mm-dd'} TIME {t 'hh-mm-ss'} DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} |
{d '2003-09-20'} {t '10-11-11'} {ts '2003-09-20 10:11:11.123'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용해야 함 | jpabook.MemberType.ADMIN |
엔티티 타입 | 엔티티 타입을 표현한다. 주로 상속과 관련해서 사용한다 | TYPE(m) = Member |
JPQL 기타
- SQL과 문법이 같은 식
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
조건식 CASE
1. 기본 CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
2. 단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
3. COALESCE
스칼라식을 하나씩 차례대로 조회해서 null이 아니면 반환한다.
사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
4. NULLIF
두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
JPQL 기본 함수
함수 | 설명 | 예제 |
CONCAT(문자1, 문자2, ...) | 문자를 합한다 | CONCAT('A','B') = AB |
SUBSTRING(문자, 위치, [길이]) | 위치부터 시작해 길이만큼 문자를 구한, 길이 값이 없으면 나머지 전체 길이를 뜻한다. | SUBSTRING('ABCDEF',2,3) = BCD |
TRIM([[LEADING|TRAINING|BOTH] [트림문자] FROM] 문자) | LEADING : 왼쪽만 TRAINING : 오른쪽만 BOTH : 양쪽 다 트림 문자를 제거한다. 기본값은 BOTH. 트림문자의 기본 값은 공백이다 | TRIM(' ABC ') = 'ABC' |
LOWER(문자) | 소문자로 변경 | LOWER('ABC') = 'abc' |
UPPER(문자) | 대문자로 변경 | UPPER('abc') = 'ABC' |
LENGTH(문자) | 문자 길이 | LENGTH('ABC') = 3 |
LOCATE(찾을 문자, 원본 문자, [검색 시작 위치]) | 검색 위치부터 문자를 검색한다 1부터 시작. 못찾으면 0 반환 |
LOCATE('DE', 'ABCDEFG') = 4 |
ABS(수학식) | 절댓값을 구한다 | ABS(-10) = 10 |
SQRT(수학식) | 제곱근을 구한다 | SQRT(4) = 2.0 |
MOD(수학식, 나눌수) | 나머지를 구한다 | MOD(4,3) = 1 |
SIZE(컬렉션 값 연관 경로식) | 컬렉션의 크기를 구한다 | SIZE(t.members) |
INDEX(별칭) | LIST 타입 컬렉션의 위치값을 구함. 단 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용할 수 있다 | t.members m where INDEX(m) > 3 |
'백엔드 > SPRING' 카테고리의 다른 글
[스프링 핵심 원리] 1. 객체 지향 설계와 스프링 (0) | 2025.03.17 |
---|---|
[JPA 프로그래밍] 10. 객체 지향 쿼리 언어2 - 중급 문법 (0) | 2025.02.16 |
[JPA 프로그래밍] 8. 값 타입 (0) | 2025.02.07 |
[JPA 프로그래밍] 7. 프록시와 연관관계 관리 (0) | 2025.02.06 |
[JPA 프로그래밍] 6. 고급 매핑 (0) | 2025.02.05 |
객체 지향 쿼리 언어 소개
EntityManager.find()나 객체 그래프 탐색은 가방 단순한 검색 방법이다. 하지만 이 기능만으로는 애플리케이션을 개발하기 어렵다. 예를 들어, 나이가 18살 이상인 회원을 모두 검색하고 싶자면 좀 더 현실적이고 복잡한 검색 방법이 필요하다. 그렇다고 해서 모든 회원 엔티티를 메모리에 올려두고 검색하는 것은 현실성이 없다. 결국 데이터는 데이터베이스에 있으므로 SQL로 필요한 내용을 최대한 걸러서 조회해야 한다. 하지만 ORM을 사용하면 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로 검색도 엔티티 객체를 대상으로 하는 방법이 필요하다.
JPQL은 이러한 문제를 해결하기 위해서 만들어졌다.
- 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다.
- SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면 JPQL은 엔티티 객체를 대상으로 하는 객체 지향 쿼리이다. JPQL을 사용하면 JPA는 이 JPQL을 분석한 다음 적절한 SQL을 만들어 데이터베이스를 조회한다. 그리고 조회한 결과로 엔티티 객체를 생성해서 반환한다.
JPA는 JPQL 뿐만 아니라 다양한 검색 방법을 제공한다.
- JPQL
- Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다
다음은 JPA가 공식 지원하는 기능은 아니지만, 알아둘 가치가 있다
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크다
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하면 JDBC를 직접 사용할 수 있다.
JPQL 소개
JPQL은 테이블이 아닌 객체를 대상으로 검색하는 객체 지향 쿼리이다. SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다. JPQL을 한마디로 정의하면 객체 지향 SQL이다.
// 검색
String jpql = "select m from Member m where m.age > 18";
List<Member> result = em.createQuery(jpql, Member.class) .getResultList();
// 실행된 SQL
select
m.id as id,
m.age as age,
m.USERNAME as USERNAME,
m.TEAM_ID as TEAM_ID
from
Member m
where
m.age>18
Criteria 소개
Criteria는 JPQL을 생성하는 빌더 클래스다. Criteria의 장점은 문자가 아닌 query.select(m).where(..)처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
- 컴파일 시점에 오류를 발견할 수 있다
- IDE를 사용하면 코드 자동완성을 지원한다
- 동적 쿼리를 작성하기 편하다
//Criteria 사용 준비
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> query = cb.createQuery(Member.class);
//루트 클래스 (조회를 시작할 클래스)
Root<Member> m = query.from(Member.class);
//쿼리 생성 CriteriaQuery<Member> cq =
query.select(m).where(cb.equal(m.get("username"), “kim”));
List<Member> resultList = em.createQuery(cq).getResultList();
Criteria가 가진 장점이 많지만 모든 장점을 상쇄할 정도로 복잡하고 장황하다. 따라서 사용하기 불편한 것은 물론이고, Criteria로 작성한 코드도 한눈에 들어오지 않는다는 단점이 있다
QueryDSL 소개
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다. QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다. 뿐만 아니라 동적 쿼리를 작성하기 편하다.
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery query = new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list =
query.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch();
네이티브 SQL 소개
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는데 이것을 네이티브 SQL이라고 한다. JPQL을 사용해도 가끔씩 특정 데이터베이스에 의존하는 기능을 사용해야 할 때가 있는데 그 때 사용한다.
String sql =
“SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList =
em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
JDBC나 마이바티슬르 JPA와 함께 사용하면 영속성 컨텍스트를 적절한 시점에 강제로 플러시해야 한다. JDBC를 직접 사용하든 마이바티스 같은 SQL 매퍼와 사용하든 모두 JPA를 우회해서 데이터베이스에 접근한다. 문제는 JPA를 우회하는 SQL에 대해서는 JPA가 전혀 인식하지 못한다는 점이다.
이런 이슈를 해결하는 방법은 JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트를 수동으로 플러시해서 데이터베이스와 영속성 컨텍스트를 동기화하면 된다.
JPQL(Java Persistence Query Language)
JPQL 소개
- JPQL은 객체 지향 쿼리 언어이다. 따라서 테이블을 대상으로 쿼리하는 것이 아니라, 엔티티 객체를 대상으로 쿼리한다.
- JPQL은 SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
- JPQL은 결국 SQL로 변환된다.
JPQL 문법
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE문을 사용할 수 있다. 엔티티를 저장할 때 EntityManager.persist() 메소드를 사용하면 되기 때문에 INSERT문은 따로 존재하지 않는다
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자 구분을 한다 (객체에 있는 이름 그대로 작성해야 함 : Member, age)
- JPQL 키워드는 대소문자 구분 하지 않는다 (SELECT, FROM, where)
- JPQL에서 사용한 Member는 테이블 이름이 아니라, 엔티티의 이름이다.
엔티티 명을 지정하지 않으면 클래스명을 기본 값으로 사용한다. - 별칭은 필수이다. (m) (단, 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");
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때, 리스트 반환
- 결과가 없으면 빈 리스트 반환 - query.getSingleResult() : 결과가 정확히 하나, 단일 객체 반환
- 결과가 없으면 : javax.persistence.NoResultException
- 결과가 둘 이상이면 : javax.persistence.NonUniqueResultException
파라미터 바인딩
- 이름 기준 파라미터
파라미터를 이름으로 구분하는 방법이다. 이름 기준 파라미터는 앞에 :를 사용한다.
SELECT m FROM Member m where m.username=:username
query.setParameter("username", usernameParam);
- 위치 기준 파라미터
? 다음에 위치 값을 주면 된다. 위치 값은 1부터 시작된다.
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
프로젝션
프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것이다. [SELECT {프로젝션 대상} FROM]으로 대상을 선택한다. 프로젝션 대상은 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
프로젝션 대상
- 엔티티 프로젝션
이렇게 조회한 엔티티는 영속성 컨택스트에서 관리한다.
SELECT m FROM Member m; // 회원
SELECT m.team FROM Member m; // 팀
- 임베디드 타입 프로젝션
임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다. 임베디드 타입은 엔티티 타입이 아닌 값 타입이다. 따라서 이렇게 직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리하지 않는다.
SELECT m.address FROM Member m;
- 스칼라 타입 프로젝션
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라고 한다. 중복 데이터를 제거하려면 DISTINCT를 사용한다.
SELECT m.username, m.age FROM Member m;
SELECT DISTINCT username FROM Member m;
프로젝션 여러 값 조회
엔티티를 대상으로 조회하면 편하겠지만, 꼭 필요한 데이터들만 선택해서 조회해야 할 때도 있다. 프로젝션에 여러 값을 선택하면 TypeQuery 대신 Query를 사용해야 한다.
1. Query 타입으로 조회
List resultList = em
.createQuery("select m.username, m.age from Member m")
.getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
2. Object 타입으로 조회
List<Object[]> resultList = em
.createQuery("select m.username, m.age from Member m")
.getResultList();
Object[] result = resultList.get(0);
3. new 명령어로 조회
List<MemberDTO> resultList = em
.createQuery("select new jpql.MemberDTO(m.username, m.age) from Member m", MemberDTO.class)
.getResultList();
MemberDTO result = resultList.get(0);
페이징 API
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();
조인
조인 종류
- 내부 조인
내부 조인은 INNER JOIN을 사용한다. 참고로 INNER는 생략 가능하다
String teamName = "팀A";
String query = "SELECT m FROM Member m INNER JOIN m.team t "
+ "WHERE t.name = :teamName";
List<Member> members = em.createQuery(query, Member.class)
.setParameter("teamName", teamName)
.getResultList();
- 외부 조인
OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
SELECT m FROM Member m LEFT [OUTER] JOIN m.team t
- 세타 조인
WHERE 절을 사용해서 세타 조인을 할 수 있다. 세타 조인은 내부 조인만 지원한다.
select count(m) from Member m, Team t where m.username = t.name
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
서브 쿼리 지원 함수
- EXISTS
- 문법 : [NOT] EXISTS (subquery)
- 설명 : 서브 쿼리에 결과가 존재하면 참이다. NOT은 반대 - {ALL / ANY / SOME}
- 문법 : {ALL / ANY / SOME} (subquery)
- 설명 : 비교 연산자와 같이 사용한다.
-> ALL : 조건을 모두 만족하면 참이다
-> ANY 혹은 SOME : 둘은 같은 의미다. 조건을 하나라도 만족하면 참 - IN
- 문법 : [NOT] IN (subquery)
- 설명 : 서브 쿼리의 결과 중 하나라도 같은 것이 있으면 참이다. 참고로 IN은 서브 쿼리가 아닌 곳에서도 사용한다.
팀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는 WHERE, HAVING 절에서만 서브 쿼리 사용 가능하다
- SELECT 절도 가능하다 (하이버네이트에서 지원)
- FROM 절의 서브 쿼리는 현재 JPQL에서 불가능하다 (조인으로 풀 수 있으면 풀어서 해결)
JPQL 타입 표현
종류 | 설명 | 예제 |
문자 | 작은 따옴표 사이에 표현 작은 따옴표를 표현하고 싶으면 작은 따옴표 연속 두개('') 사용 |
'HELLO' 'She''s' |
숫자 | L(Long 타입 지정) D(Double 타입 지정) F(Float 타입 지정) |
10L 10D 10F |
날짜 | DATE {d 'yyyy-mm-dd'} TIME {t 'hh-mm-ss'} DATETIME {ts 'yyyy-mm-dd hh:mm:ss.f'} |
{d '2003-09-20'} {t '10-11-11'} {ts '2003-09-20 10:11:11.123'} |
Boolean | TRUE, FALSE | |
Enum | 패키지명을 포함한 전체 이름을 사용해야 함 | jpabook.MemberType.ADMIN |
엔티티 타입 | 엔티티 타입을 표현한다. 주로 상속과 관련해서 사용한다 | TYPE(m) = Member |
JPQL 기타
- SQL과 문법이 같은 식
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL
조건식 CASE
1. 기본 CASE 식
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
2. 단순 CASE 식
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
3. COALESCE
스칼라식을 하나씩 차례대로 조회해서 null이 아니면 반환한다.
사용자 이름이 없으면 이름 없는 회원을 반환
select coalesce(m.username,'이름 없는 회원') from Member m
4. NULLIF
두 값이 같으면 null 반환, 다르면 첫 번째 값 반환
사용자 이름이 ‘관리자’면 null을 반환하고 나머지는 본인의 이름을 반환
select NULLIF(m.username, '관리자') from Member m
JPQL 기본 함수
함수 | 설명 | 예제 |
CONCAT(문자1, 문자2, ...) | 문자를 합한다 | CONCAT('A','B') = AB |
SUBSTRING(문자, 위치, [길이]) | 위치부터 시작해 길이만큼 문자를 구한, 길이 값이 없으면 나머지 전체 길이를 뜻한다. | SUBSTRING('ABCDEF',2,3) = BCD |
TRIM([[LEADING|TRAINING|BOTH] [트림문자] FROM] 문자) | LEADING : 왼쪽만 TRAINING : 오른쪽만 BOTH : 양쪽 다 트림 문자를 제거한다. 기본값은 BOTH. 트림문자의 기본 값은 공백이다 | TRIM(' ABC ') = 'ABC' |
LOWER(문자) | 소문자로 변경 | LOWER('ABC') = 'abc' |
UPPER(문자) | 대문자로 변경 | UPPER('abc') = 'ABC' |
LENGTH(문자) | 문자 길이 | LENGTH('ABC') = 3 |
LOCATE(찾을 문자, 원본 문자, [검색 시작 위치]) | 검색 위치부터 문자를 검색한다 1부터 시작. 못찾으면 0 반환 |
LOCATE('DE', 'ABCDEFG') = 4 |
ABS(수학식) | 절댓값을 구한다 | ABS(-10) = 10 |
SQRT(수학식) | 제곱근을 구한다 | SQRT(4) = 2.0 |
MOD(수학식, 나눌수) | 나머지를 구한다 | MOD(4,3) = 1 |
SIZE(컬렉션 값 연관 경로식) | 컬렉션의 크기를 구한다 | SIZE(t.members) |
INDEX(별칭) | LIST 타입 컬렉션의 위치값을 구함. 단 컬렉션이 @OrderColumn을 사용하는 LIST 타입일 때만 사용할 수 있다 | t.members m where INDEX(m) > 3 |
'백엔드 > SPRING' 카테고리의 다른 글
[스프링 핵심 원리] 1. 객체 지향 설계와 스프링 (0) | 2025.03.17 |
---|---|
[JPA 프로그래밍] 10. 객체 지향 쿼리 언어2 - 중급 문법 (0) | 2025.02.16 |
[JPA 프로그래밍] 8. 값 타입 (0) | 2025.02.07 |
[JPA 프로그래밍] 7. 프록시와 연관관계 관리 (0) | 2025.02.06 |
[JPA 프로그래밍] 6. 고급 매핑 (0) | 2025.02.05 |