객체지향 쿼리 언어 소개
JPA는 다양한 쿼리 방법을 지원
- JPQL
- JPA Criteria (거의 사용x)
- QueryDSL
- 네이티브 SQL
- JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용
JPQL
엔티티 객체를 대상으로 하는 객체지향 쿼리 언어 (테이블X, 클래스O) = 객체지향SQL
💡
JPQL: 엔티티 객체를 대상으로 쿼리
SQL: 테이블을 대상으로 쿼리
단순한 조회는
- EntityManager.find()
- 객체 그래프 탐색 (a.getB().getC())
문제는 검색쿼리
1. 모든 DB 데이터를 객체로 변환해 검색은 불가능 → 필요한 데이터만 DB에서 불러오려면 검색 조건이 포함된 SQL이 필요
2. SQL과 문법 유사 (SELECT, FROM, WHERE, GROUP BY, HAVING, JOIN 지원)
3. SQL을 추상화하여 특정 DB에 의존하지 않음
만약, 특정 조건 회원을 검색하고 싶다면?(검색 쿼리)
//em.createQuery("JPQL문", 반환타입.class)
List<Member> result = em.createQuery("select m from Member m", Member.class)
.setFirstResult(5)
.setMaxResults(8)
.getResultList();
for (Member member : result) {
System.out.println(member.getName());
}
QueryDSL
- JPQL 빌더 역할
- 문자가 아닌 자바코드로 JPQL을 작성할 수 있음
- 컴파일 시점에 문법 오류를 찾을 수 있음
- 동적쿼리 작성 편리함
- 단순하고 쉬움
- 실무 사용 권장
//JPQL
//select m from Member m where m.age > 18
JPAFactoryQuery queryFactory= new JPAQueryFactory(em);
QMember m = QMember.member;
List<Member> list = queryFactory.selectFrom(m)
.where(m.age.gt(18))
.orderBy(m.name.desc())
.fetch()
네이티브 SQL
- JPA가 제공하는 SQL을 직접 사용하는 기능
- JPQL로 해결할 수 없는 특정 데이터베이스에 의존적인 기능 ex. 오라클 CONNECT BY, 특정 DB만 사용하는 SQL 힌트
String sql =
“SELECT ID, AGE, TEAM_ID, NAME FROM MEMBER WHERE NAME = ‘kim’";
List<Member> resultList =
em.createNativeQuery(sql, Member.class).getResultList();
JDBC 직접 사용, SpringJdbcTemplate 등
- JPA를 사용하면서 JDBC 커넥션을 직접 사용하거나, SpringJdbcTemplate, MyBatis 등을 함께 사용할 수 있다.
- 단, 영속성 컨텍스트를 적절한 시점에 강제로 플러시가 필요 ex) JPA를 우회해서 SQL을 실행하기 직전에 영속성 컨텍스트 수동 플러시
JPQL - 기본 문법과 기능
JPQL은 결국 SQL로 변환된다.
(참고 예시)
![]() |
![]() |
✓ JPQL 문법
select_문 :: =
select_절
from_절
[where_절]
[groupby_절]
[having_절]
[orderby_절]
//한번에 여러개 변경할떄
update_문 :: = update_절 [where_절]
delete_문 :: = delete_절 [where_절]
- select m from Member(엔티티) as m where m.age > 18
- 엔티티와 속성은 대소문자 구분o(Member, age)
- JPQL 키워드는 대소문자 구분x (SELECT, FROM, where)
- 테이블 이름이 아닌 엔티티 이름 사용(Member)
- 별칭 필수 (m) (as는 생략 가능)
✓ 집합과 정렬
select
COUNT(m), //회원수
SUM(m.age), //나이 합
AVG(m.age), //평균 나이
MAX(m.age), //최대 나이
MIN(m.age) //최소 나이
from Member m
GROUP BY, HAVING, ORDER BY
✓ 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"); //String, int
✓ 결과 조회 API
결과가 컬렉션일 때,
- query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
결과가 없으면 빈 리스트 반환 - query.getSingleResult(): 결과가 정확히 하나!, 단일 객체 반환
결과가 없으면: javax.persistence.NoResultException
둘 이상이면: javax.persistence.NonUniqueResultException
✓ 파라미터 바인딩 - 이름 기준, 위치 기준
//이름 기준
Member result = em.createQuery(
SELECT m FROM Member m where m.username=:username, Member.class)
.setParameter("username", usernameParam)
.getSingleResult();
//위치 기준(쓰지말자)
SELECT m FROM Member m where m.username=?1
query.setParameter(1, usernameParam);
✓ 프로젝션
- SELECT 절에 조회할 대상을 지정하는 것
- 프로젝션 대상: 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자등 기본 데이터 타입)
- SELECT m FROM Member m -> 엔티티 프로젝션
- SELECT m.team FROM Member m -> 엔티티 프로젝션
- SELECT m.address FROM Member m -> 임베디드 타입 프로젝션
- SELECT DISTINCT m.username, m.age FROM Member m -> 스칼라 타입 프로젝션
- DISTINCT로 중복 제거
-여러값 조회
- SELECT m.username, m.age FROM Member m
1. Query 타입으로 조회
2. Object[] 타입으로 조회
List<Object[]> resultList = em.createQuery("SELECT DISTINCT m.username, m.age FROM Member m")
.getResultList();
Object[] result = resultList.get(0);
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
3. new 명령어로 조회 : 단순 값을 DTO로 바로 조회
public class MemberDTO {
private String username;
private int age;
public MemberDTO(String username, int age) {
this.username = username;
this.age = age;
}
// getter, setter
}
List<MemberDTO> result = em.createQuery(
"SELECT new jpql.MemberDTO(m.username, m.age) FROM Member m",
MemberDTO.class
).getResultList();
MemberDTO memberDTO = result.get(0);
System.out.println("username = " + memberDTO.getUsername());
System.out.println("memberDTO = " + memberDTO.getAge());
반환 타입이 엔티티가 아닌 DTO인 경우, SELECT new 구문을 사용하면서 패키지명을 포함한 생성자 호출을 명시해야 함.
✓ 페이징
JPA는 페이징을 다음 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치 (0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
String jpql = "select m from Member m order by m.age desc";
List<Member> resultList = em.createQuery(jpql, Member.class)
.setFirstResult(10)
.setMaxResults(20)
MySQL 방언, Oracle 방언
SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM
MEMBER M
ORDER BY
M.NAME DESC LIMIT ?, ?
SELECT * FROM
( SELECT ROW_.*, ROWNUM ROWNUM_
FROM
( SELECT
M.ID AS ID,
M.AGE AS AGE,
M.TEAM_ID AS TEAM_ID,
M.NAME AS NAME
FROM MEMBER M
ORDER BY M.NAME
) ROW_
WHERE ROWNUM <= ?
)
WHERE ROWNUM_ > ?
✓ 조인
- 내부 조인: 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
-ON절
1. 조인 대상 필터링
Ex. 회원과 팀을 조인하면서, 팀 이름이 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 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
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUsername("member1");
member.setAge(10);
member.setTeam(team);
em.persist(member);
em.flush();
em.clear();
String jpql = "select m from Member m left join m.team t on t.name = 'teamA'";
List<Member> resultList = em.createQuery(jpql, Member.class)
.getResultList();
System.out.println("resultList.size() = " + resultList.get(0).getUsername());
tx.commit();
✓ 서브쿼리
- 나이가 평균보다 많은 회원
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 절에서만 서브 쿼리 사용 가능
- 하이버네이트에서는 SELECT절도 가능
- Hibernate 6부터는 FROM 절의 서브쿼리도 지원함
- 다른 JPA 구현체에서는 여전히 FROM 절 서브쿼리가 불가능할 수 있으므로
조인으로 풀 수 있으면 조인으로 해결하는 것이 호환성 면에서 안전
- 다른 JPA 구현체에서는 여전히 FROM 절 서브쿼리가 불가능할 수 있으므로
✓ JPQL 타입 표현
- 문자: ‘HELLO’, ‘She’’s’
- 숫자: 10L(Long), 10D(Double), 10F(Float)
- Boolean: TRUE, FALSE
- ENUM: jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입: TYPE(m) = Member (상속 관계에서 사용)
Member member = new Member();
member.setUsername("teamA");
member.setAge(10);
member.setType(MemberType.ADMIN); //
em.persist(member);
String query = "select m.username, 'HELLO', TRUE From Member m "
+ "where m.type = jpql.MemberType.ADMIN";
간단 작성
String query = "select m.username, 'HELLO', TRUE From Member m "
+ "where m.type = :userType";
List<Object[]> resultList = em.createQuery(query)
.setParameter("userType",MemberType.ADMIN)
.getResultList();
상속관계
String query = ("select i from Item i where type(i) = Book", Item.class)
✓ JPQL 기타
- SQL과 문법이 같은 식
- EXISTS, IN
- AND, OR, NOT
- =, >, >=, <, <=, <>
- BETWEEN, LIKE, IS NULL, IS NOT 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: 하나씩 조회해서 null이 아니면 반환
select coalesce(m.username,'이름 없는 회원') from Member m
NULLIF: 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
select NULLIF(m.username, '관리자') from Member m
✓ JPQL 기본 함수
- CONCAT ex) select concat('a','b') from Member m
- SUBSTRING ex) select substring(m.name,2, 3) from Member m
- TRIM ex) select trim(m.name) from Member m -양쪽 공백 제거
- LOWER, UPPER ex) select lower(m.name) from Member m - 소문자
- LENGTH ex) select length(m.name) from Member m
- LOCATE ex) select locate('a', m.name) from Member m - 'a' 처음 등장하는 위치
- ABS, SQRT, MOD ex) select abs(m.username,2, 3) from Member m -절댓값,제곱근,나머지
- SIZE(JPA 용도) : 컬렉션(1:N 관계)의 크기(개수) 반환 ex) SELECT SIZE(t.members) FROM Team t
- INDEX(JPA 용도) : @OrderColumn 사용 시 인덱스 반환 ex) SELECT INDEX(o) FROM Member m JOIN m.orders o(쓰지마)
✓ 사용자 정의 함수 호출
- 하이버네이트는 사용전 방언에 추가해야 한다.
- 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
select function('group_concat', i.name) from Item i
'JPA' 카테고리의 다른 글
| [JPA] 객체지향 쿼리 언어 - 중급 문법 (0) | 2025.04.20 |
|---|---|
| [JPA] 값 타입 (0) | 2025.04.16 |
| [JPA] 영속성 전이와 고아 객체 (0) | 2025.04.15 |
| [JPA] 프록시와 지연 로딩 전략 (0) | 2025.04.09 |
| [JPA] 고급 매핑(상속) (0) | 2025.04.06 |

