JPQL 소개
JPQL은 객체지향 쿼리 언어다. → 테이블을 대상으로 쿼리하는 것이 아닌, 엔티티 객체를 대상으로 쿼리 한다.
예시 비교 모델
Member 엔티티
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String userName;
private int age;
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;
}
Team 엔티티
@Entity
public class Team {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "team")
private List<Member> members = new ArrayList<>();
}
Order 엔티티
:ORDER는 예약어로 인해 오류가 발생할 수 있으므로 ORDERS로 테이블명을 변경하자.
@Entity
@Table(name = "ORDERS")
public class Order {
@Id @GeneratedValue
private Long id;
private int orderAmount;
@Embedded
private Address address;
@ManyToOne
@JoinColumn(name = "PRODUCT_ID")
private Product product;
}
Address 엔티티
package jpql;
import javax.persistence.Embeddable;
@Embeddable
public class Address {
private String city;
private String street;
private String zipcode;
}
Product 엔티티
@Entity
public class Product {
@Id @GeneratedValue
private Long id;
private String name;
private int price;
private int stockAmount;
}
JPQL 기본 문법
- select m from Member as m where m.age > 18
- 엔티티와 속성은 대소문자를 구분한다. (Member 와 age 등)
- JPQL 키워드는 대소문자를 구분하지 않는다.(SELECT, FROM, WHERE)
- 테이블 이름이 아닌 엔티티 이름이며, 별칭(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
TypedQuery<Member> query1 = em.createQuery("select m from Member m", Member.class);
TypedQuery<String> query2= em.createQuery("select m.userName from Member m", String.class);
Query query = em.createQuery("select m.userName, m.age from Member m");
- TypeQuery : 반환 타입이 명확할 때 사용
- Query : 반환 타입이 명확하지 않을 때 사용
결과 조회 API
- query.getResultList() : 결과가 하나 이상일 때는 리스트를 반환하고, 없으면 빈 리스트를 반환한다.
- query.getSingleResult() : 결과가 정확히 하나일 때 단일 객체를 반환한다.
- 결과가 없으면 : NoResultException 예외 발생
- 결과가 둘 이상이면 : NonUniqueResultException 예외 발생
파라미터 바인딩 - 이름 기준, 위치 기준
이름 기준
Member member = new Member();
member.setUserName("member1");
em.persist(member);
List<Member> result = em.createQuery("select m from Member m where m.userName = :userName", Member.class)
.setParameter("userName", "member1")
.getResultList();
for (Member mem : result) {
System.out.println("mem = " + mem.getUserName());
}
위치 기준
List<Member> result = em.createQuery("select m from Member m where m.userName = ?1", Member.class)
.setParameter(1, "member1")
.getResultList();
- 위치 기준은 수정할 때 인덱싱 바꾸기가 불편하므로 이름 기준으로 사용하도록 하자.
프로젝션(SELECT)
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라고 한다. 프로젝션 대상으로는 엔티티, 임베디드 타입, 스칼라 타입(숫자, 문자 등 기본 데이터 타입)이 있다.
- SELECT m FROM Member m : 엔티티 프로젝션
- SELECT m.team FROM Member m : 엔티티 프로젝션
- SELECT m.address FROM Member m : 임베디드 타입 프로젝션
- SELECT m.userName, m.age FROM Member m : 스칼라 타입 프로젝션
- DISTINCT로 중복 제거
여러 값 조회
1. Query 타입으로 조회
List resultList = em.createQuery("select m.userName, m.age from Member m").getResultList();
Object o = resultList.get(0);
Object[] result = (Object[]) o;
System.out.println("userName = " + result[0]);
System.out.println("age = " + result[1]);
2. Object 타입으로 조회
List<Object[]> resultList = em.createQuery("select 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;
}
}
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("memberDTO = " + memberDTO.getUserName());
System.out.println("memberDTO = " + memberDTO.getAge());
createQuery 부분을 자세히보면 DTO 클래스를 매핑하기 위해 new 디렉토리 위치를 넣을 것을 알 수 있다. 즉, 패키지 명을 포함한 전체 클래스명을 입력해야하며, 순서와 타입이 일치하는 생성자가 필요하다.
페이징
- JPA는 페이징을 다음 두 API로 추상화한다.
- setFirstResult(int startPosition) : 조회 시작 위치
- setMaxResults(int maxResult) : 조회할 데이터 수
예시
List<Member> result = em.createQuery("select m from Member m order by m.age desc", Member.class)
.setFirstResult(0)
.setMaxResults(10)
.getResultList();
System.out.println("result.size = " + result.size());
for (Member member1 : result) {
System.out.println("member = " + member1);
}
데이터베이스 방언들을 보자.
//Oracle
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_ > ?
//mysql
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 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
조인 예제를 위한 연관관계 설정
: 회원과 팀사이의 연관 관계에서 둘다 값을 넣어주기 위해 값을 넣어주자.
Getter & Setter는 들어가있으나 생략했다.
@Entity
public class Member {
...
public void changeTeam(Team team) { // 양방향 연관 관계에서 Team에도 데이터가 자동으로 들어가게 설정
this.team = team;
team.getMembers().add(this);
}
}
inner join
Team team = new Team();
team.setName("teamA");
em.persist(team);
Member member = new Member();
member.setUserName("member1");
member.setAge(10);
em.persist(member);
em.flush();
em.clear();
String query = "select m from Member m inner join m.team t";
List<Member> result = em.createQuery(query, Member.class).getResultList();
tx.commit();
- inner는 생략 가능하다.
외부 조인
String query = "select m from Member m left outer join m.team t";
- 외부 조인도 outer 를 생략 가능 하다.
세타 조인
String query = "select m from Member m, Team t where m.userName=t.name";
- 데이터가 곱해져서 모든 경우의 값을 출력한다.
조인 - ON 절
ON절을 활용한 조인은 (JPA 2.1부터 지원한다.)
- 조인 대상 필터링
- 연관관계 없는 엔티티 외부 조인 (하이버네이트 5.1부터)
1. 조인 대상 필터링
- 회원과 팀을 조인하면서, 팀 이름이 A인 팀만 조인
String query = "select m from Member m left join m.team t on t.name = 'A'";
List<Member> result = em.createQuery(query, Member.class).getResultList();
System.out.println("result.size() = " + result.size());
2. 연관관계 없는 엔티티 외부 조인
String query = "select m from Member m left join Team t on m.userName = t.name";
List<Member> result = em.createQuery(query, Member.class).getResultList();
System.out.println("result.size() = " + result.size());
서브 쿼리
- 나이가 평균보다 많은 회원
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}
- 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는 WHERE, HAVING 절에서만 서브 쿼리를 사용할 수 있다.
- 하이버네이트에선 SELECT 절도 가능하다.
- FROM절의 서브 쿼리는 JPQL에서 불가능 → 조인으로 풀 수 있으면 풀어서 해결하자.
JPQL 타입 표현
JPQL 타입 표현
- 문자 : 'HELLO', 'She''s'
- 숫자 : 10L, 10D, 10F
- boolean : TRUE, FALSE
- ENUM : jpabook.MemberType.Admin (패키지명 포함)
- 엔티티 타입 : TYPE(m) = Member (상속 관계에서 사용)
조건식(CASE 등)
CASE 식
조건식이란? 쿼리 안에서 분기를 하는 것이다. 조건식은 기본 CASE 식이랑 단순 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이 아니면 반환
NULLIF : 두 값이 같으면 null 반환, 다르면 첫번째 값 반환
사용자 이름이 없으면 이름 없는 회원을 반환
사용자 이름이 '관리자'면 null을 반환하고 나머지는 본인의 이름을 변환
JPQL 함수
JPQL은 CONCAT, SUBSTRING, TRIM, LOWER, UPPER, LENGTH, LOCATE, ABS, SQRT, MOD, SIZE, INDEX(JPA 용도) 등 지원을 해준다.
CONCAT
String query = "select 'a'||'b' From Member m"; // 하이버네이트 방식 지원
String query = "select concat('a','b') From Member m";
SUBSTRING
String query = "select substring(m.username,2,3) From Member m";
LOCATE
String query = "select locate('de','abcdefg') From Member m";
- 해당 시작 인덱스(4) 반환
SIZE
String query = "select locate('de','abcdefg') From Member m";
- 컬렉션 사이즈 반환
사용자 정의 함수 호출
select function('group_concat', i.name) from Item i
- 하이버네이트는 사용전 방언에 추가해야 한다. : 사용하는 DB 방언을 상속받고, 사용자 정의 함수를 등록한다.
이 글은 김영한 님의 "자바 ORM 표준 JPA 프로그래밍 - 기본 편" 강의를 듣고 정리한 내용입니다.