객체지향 쿼리
EntityManager.find() 메소드를 사용하면 식별자로 엔티티 하나를 조회할 수 있다. 하지만 이 기능만으로 애플리케이션을 개발하기는 어렵다.
객체지향 쿼리 소개
다음은 JPA가 공식 지원하는 기능이다.
- JPQL
- Criteria 쿼리 : JPQL을 편하게 작성하도록 도와주는 API, 빌더 클래스 모음
- 네이티브 SQL : JPA에서 JPQL 대신 직접 SQL을 사용할 수 있다.
다음은 JPA가 공식 지원하지는 않지만 알아둘 가치가 있다.
- QueryDSL : Criteria 쿼리처럼 JPQL을 편하게 작성하도록 도와주는 빌더 클래스 모음, 비표준 오픈소스 프레임워크
- JDBC 직접 사용, MyBatis 같은 SQL 매퍼 프레임워크 사용 : 필요하면 JDBC를 직접 사용할 수 있다.
JPQL
특징
- 객체지향 쿼리 언어
- 테이블 대상이 아닌 엔티티 객체를 대상으로 쿼리
- SQL을 추상화하여 특정 DB SQL에 의존하지 않음
- SQL로 변환되어 실행
- JPQL API는 대부분 메소드 체인 방식으로 설계되어있어서 연속으로 작성 가능
1. 쿼리 작성
select 구문 구조
select [결과로 리턴할 엔티티 혹은 엔티티의 특정 필드]
from [조회 대상 엔티티]
where [조건]
-- 아래는 옵션
groupby [그룹화할 기준]
having [각 그룹에 대한 조건]
orderby [정렬 기준]
기본 문법
- 대소문자 구분
- 엔티티와 속성은 대소문자를 구분한다. 반면에 select,from과 같은 키워드는 대소문자를 구분하지 않는다.
- 엔티티 이름
- @Entity(name="xxx") 어노테이션으로 엔티티명을 별도로 설정한 경우에는 엔티티 클래스명이 아닌 해당 name 속성명을 사용해야 한다.
@Entity(name="members")
class Member{
}
select m.username from members m -- (O)
select m.username from Member m -- (x)
- 별칭 필수
- from절에서 엔티티에 대해 별칭을 설정해야한다. ( as 는 생략할 수 있다.)
select m.username from Member as m-- (O)
select username from Member-- (x)
TypeQuery, Query
- 작성한 JPQL 실행 위해 쿼리 객체 필요
- 반환할 타입 명확하게 지정 가능하면 TypeQuery 객체 사용
- 반환 타입 명확하게 지정 불가능하면 Query 객체 사용
public void useTypeQuery(EntityManager em) {
TypeQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List<Member> resultList = query.getResultList();
for (Member member: resultList) {
System.out.println("member = " + member);
}
}
...
public void useQuery(EntityManager em) {
Query query = em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
for (Object o: resulstList) {
Object[] result = (Object[]) o; // 결과가 둘 이상이면 Object[] 반환System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
}
결과 조회
- 아래 메소드들 호출시 실제 쿼리 실행하여 DB 조회함
- query.getResultList(): 결과를 반환. 없으면 빈 컬렉션 반환
- query.getSingleResult(): 결과가 정확히 하나일 때 사용
- 결과 없으면 javax.persistence.NoResultException 발생
- 결과가 1개보다 많으면 javax.persistence.NoUniqueResultException 발생
파라미터 바인딩
- JPQL은 이름 기준 파라미터 바인딩 지원
- JDBC는 위치 기준 파라미터 바인딩만 지원
- 파라미터 바인딩을 사용하지 않고 직접 문자 더해 만들어 넣으면 SQL 인젝션 공격 당할 수 있음
- 또한 파라미터 바인딩 방식 쓰면 파리미터 값 달라도 같은 쿼리로 인식해 JPQL을 SQL로 파싱한 결과를 재사용 할 수 있음
이름 기준 파라미터
- 파라미터를 이름으로 구분하는 방법
- 이름 기준 파라미터는 앞에 : 붙임
Strign usernameParam = "User1";
List<Member> resulstList = em
.createQuery("SELECT m FROM Member m WHERE m.username = :username", Member.class) // :username이 이름 기준 파라미터 정의
.setParameter("username", usernameParam); // setParameter로 파라미터 바인딩
.getResultList();
위치 기준 파라미터 (권장은 x)
- 파라미터를 위치 기준으로 구분하는 방법
- ? 다음에 위치 값 주면 됨
- 위치 값은 1부터 시작
List<Member> members = em
.createQuery("SELECT m FROM Member m WHERE m.username = ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
프로젝션
- SELECT 절에 조회할 대상을 지정하는 것 = projection
- SELECT {프로젝션 대상} FROM 으로 대상 선택
- 대상으로는 엔티티, 임베디드 타입, 스칼라 타입이 있음
엔티티 프로젝션
SELECT m FROM Member m // Member
SELECT m.team FROM Member m // Team
- 위 예시처럼 엔티티를 프로젝션 대상으로 사용해서 원하는 객체를 바로 조회
- 이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리됨
임베디드 타입 프로젝션
List<Address> address = em
.createQuery("SELECT o.address FROM Order o", Address.class)
.getResultList();
- 임베디드 타입은 조회의 시작점이 될 수 없음
- 위 예시처럼 엔티티를 통해서 조회해야 함
- 임베디드 타입은 값 타입이기 때문에 영속성 컨텍스트에서 관리 X
스칼라 타입 프로젝션
List<String> usernames = em
.createQuery("SELECT username FROM Member m", String.class)
.getResultList();
- 스칼라 타입 역시 영속성 컨텍스트에서 관리 x
- 숫자, 문자, 날짜와 같은 기본 데이터 타입들
- AVG, SUM 같은 통계 쿼리도 주로 스칼라 타입으로 조회함
여러 값 조회
List<Object[]> resultList = em
.createQuery("SELECT o.member, o.product, o.orderAmount FROM Order o")
.getResultList();
for (Object[] row: resultList) {
Member member = (Member) row[0]; // 엔티티
Product product = (Product) row[1]; // 엔티티
int orderAmount = (Integer) row[2]; // 스칼라
}
- 프로젝션에 여러 값 선택시 TypeQuery 대신 Query 써야 함
- 엔티티 타입도 여러 값 함께 조회 가능하며, 이 엔티티들도 영속성 컨텍스트로 관리 됨
NEW 명령어
TypeQuery<UserDTO> query = em.createQuery("SELECT new com.example.hellojpa.UserDTO(m.username, m.age) FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
- SELECT 다음에 NEW 명령어 사용하여 반환받을 클래스 지정하고 해당 클래스의 생성자에 JPQL 조회 결과 넘겨줄 수 있음
- 이렇게 하면 TypeQuery 사용해서 바로 변환된 객체로 리턴받을 수 있음
- NEW 명령어 사용시 주의점
- 패키지 명을 포함한 전체 클래스 명 써야함
- 순서와 타입이 일치하는 생성자 있어야 함
페이징 API
- JPA는 페이징을 아래 두 API로 추상화
- setFirstResult(int startPosition) : 조회 시작 위치(0부터 시작)
- setMaxResults(int maxResult) : 조회할 데이터 수
- DB dialect에 따라 사용중인 DB에 알맞은 SQL로 자동 변환되어 쿼리 수행됨
- 최적화 더 하려면 Native SQL 사용해야 함
List<Member> resultList = em
.createQuery("SELECT m FROM Member m ORDER BY m.username DESC", Member.class)
.setFirstResult(10); // 조회 시작 위치는 11번째(0번째가 시작이므로)
.setMaxResults(20); // 조회할 데이터 수는 20개
.getResultList(); // 결과적으로 11~30번 데이터가 조회됨
조인
• SQL 조인과 기능 동일, 문법만 약간 상이함
내부 조인
String teamName = "teamA";
List<Member> members = em
.createQuery("SELECT m
FROM Member m
INNER JOIN m.team t
WHERE t.name = :teamName", Member.class)
.setParameter("teamName", teamName)
.getResultList();
- INNSER JOIN 사용, INNER 는 생략 가능
- JPQL 은 연관 필드를 사용하여 조인함
- 연관 필드는 다른 엔티티와 연관관계를 가지기 위해 사용하는 필드
- 위 예제의 경우 m.team 이 연관필드임
외부 조인
SELECT m
FROM Member m
LEFT OUTER JOIN m.team t
- OUTER 는 생략 가능
- 마찬가지로 연관필드 가지고 조인
세타 조인
SELECT count(m)
FROM Member m, Team t
WHERE m.username = t.name // WHERE 절에서 전혀 관계 없는 Member.username 과 Team.name 을 조인함
- WHERE 절을 사용해서 세타 조인 가능
- 내부 조인만 지원함
- 세타 조인을 사용해 전혀 관계없는 엔티티도 조인 가능
서브 쿼리
- WHERE, HAVING 절에만 사용 가능, SELECT, FROM 절에선 못 씀
- 하이버네이트 HQL은 SELECT 절 서브쿼리 까지는 허용
// 나이가 평균보다 많은 회원 조회
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
// 한 건이라도 주문한 고객 조회 (컬렉션 크기 값 구할 수 있는 size 기능 활용, 실행 쿼리는 위에거랑 동일)
SELECT m
FROM Member m
WEHRE m.orders.size > 0
서브 쿼리 함수
- EXISTS
- [NOT] EXISTS (subquery)
- subquery에 결과 존재하면 참, NOT 붙으면 반대
- ALL, ANY, SOME
- {ALL | ANY | SOME} (subquery)
- ALL : 조건 모두 만족하면 참
- ANY, SOME : 조건 하나라도 만족하면 참
- IN
- [NOT] IN (subquery)
- subquery 결과 중 하나라도 같은 것 있으면 참
조건식
JPQL 타입 표현
종류 | 설명 | 예제 |
문자 | 작은 따옴표 사이에 표현작은 따옴표 표현하려면 연속 두개 사용(‘’) | ‘HELLO’‘She’’s’ |
숫자 | L(Long)D(Double)F(Float) | 10L10D10F |
날짜 | DATE { d ‘yyyy-mm-dd’ } TIME { t ‘hh-mm-ss’ } DATETIME { ts ‘yyyy-mm-dd hh:mm:ss.f’ } |
{ d ‘2021-03-03’ } { t ‘11-11-11’ } { ts ‘2021-03-03 11-11-11.111’ } m.createDate = { d ‘ 2021-03-03’ } |
Boolean | True, False | |
Enum | 패키지명 포함 전체 이름 사용 | jpabook.MemberType.Admin |
엔티티 타입 | 엔티티의 타입을 표현. 주로 상속과 관련하여 사용 | TYPE(m) = Member |
JPQL 기타
SQL과 문법이 식
• EXISTS, IN
• AND, OR, NOT
• =, >, >=, <, <=, <>
• BETWEEN, LIKE, IS NULL
CASE 식
- 특정 조건에 따라 분기할 떄 사용
- 기본 CASE
CASE
{WHEN <조건식> THEN <스칼라식>}+
ELSE <스칼라식>
END
// 예제
SELECT
CASE
WHEN m.age <= 10 then '학생요금'
WHEN m.age >= 60 then '경로요금'
ELSE '일반요금'
END
FROM Member m
- 단순(심플) CASE
- 조건식 사용 X, 문법 단순
- 자바의 switch case 문과 비슷
CASE <조건대상>
{WHEN <스칼라식1> THEN <스칼라식2>}+
ELSE <스칼라식>
END
// 예제
SELECT
CASE t.name
WHEN 'teamA' then '인센티브110%'
WHEN 'teamB' then '인센티브120%'
ELSE '인센티브105%'
END
FROM Team t
JPQL 기본 함수
- CONCAT
String query = "select concat('a', 'b') from Member m";
List<String> result = em.createQuery(query, String.class)
.getResultList();
for (String s : result) {
System.out.println("s = " + s);
}
// s = ab 출력된다.
- SUBSTRING
String query = "select substring(m.username, 2, 3) from Member m";
- TRIM
- LOWER, UPPER
- LENGTH
- LOCATE
- ABS, SQRT, MOD
- SIZE, INDEX (JPA 용도)
'Develop > JPA' 카테고리의 다른 글
[JPA/기본편] 값 타입 (0) | 2024.03.20 |
---|---|
[JPA/기본편] 프록시 (0) | 2024.03.20 |
[JPA/기본편] 상속 관계 매핑 (0) | 2024.03.20 |
[JPA/기본편] 연관관계 매핑 (0) | 2024.03.20 |
[JPA/기본편] 연관관계 (1) | 2024.03.20 |
댓글