김영한님의 인프런 강의 '자바 ORM 표준 JPA 프로그래밍'을 참고했습니다.
이번 글에서 예제로 사용할 도메인 모델이다.
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다. EntityManager.persist() 메서드를 사용하면 되므로 INSERT 문은 없다. JPQL에서 UPDATE, DELETE 문은 벌크 연산이라 하는데 뒤에서 다룰 예정이다. SELECT문을 자세히 알아보자.
SELECT문은 다음과 같이 사용한다.
select m from Member as m where m.age > 18
엔티티와 속성은 대소문자 구분 O (Member, age)
JPQL 키워드는 대소문자 구분 X(SELECT, FROM, where)
테이블 이름이 아닌 엔티티 이름 사용(Member)
별칭은 필수(m)(as는 생략 가능)
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
Query query = em.createQuery("SELECT m.username, m.age from Member m"); //username은 string, age는 int
query.getResultList(): 결과가 하나 이상일 때, 리스트 반환
query.getSingleResult(): 결과가 정확히 하나, 단일 객체 반환
📌 query.getSingleResult()는 결과가 1개가 아니면 예외가 터지므로 주의해야 한다!
String usernameParam = "User1";
TypedQuery<Member> query = em.createQuery("SELECT m FROM Member m
where m.username = :username", Member.class);
query.setParameter("username", usernameParam);
List<Member> resultList = query.getResultList();
참고로 JPQL API는 대부분 매서드 체인 방식으로 설계되어 있어서 다음과 같이 연속해서 작성할 수 있다.
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username = :username", Member.class)
.setParameter("username", usernameParam)
.getResultList();
List<Member> members =
em.createQuery("SELECT m FROM Member m where m.username
= ?1", Member.class)
.setParameter(1, usernameParam)
.getResultList();
📌 위치 기준 파라미터는 잘 사용하지 않는다. 이름 기준 파라미터를 쓰자!!
프로젝션이란 SELECT 절에 조회할 대상을 지정하는 것을 의미한다.
SELECT m FROM Member m //회원
SELECT m.team FROM Member m //팀
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
SELECT m.address FROM Member m
임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
SELECT m.age FROM Member m
숫자, 문자, 날짜와 같은 기본 데이터 타입들을 스칼라 타입이라 한다.
프로젝션에 여러 값을 선택하면 TypeQuery를 사용할 수 없고 대신에 Query를 사용해야 한다.
Query query =
em.createQuery("SELECT m.username, m.age FROM Member m");
List resultList = query.getResultList();
Iterator iterator = resultList.iterator();
while (iterator.hasNext()) {
Object[] row = (Object[]) iterator.next();
String username = (String) row[0];
Integer age = (Integer) row[1];
}
List<Object[]> resultList =
em.createQuery("SELECT m.username, m.age FROM Member m")
.getResultList();
for (Object[] row : resultList) {
String username = (String) row[0];
Integer age = (Integer) row[1];
}
스칼라 타입뿐만 아니라 엔티티 타입도 여러 값을 함께 조회할 수 있다.
SELECT 다음에 new 명령어를 사용하면 반환받을 클래스를 지정할 수 있는데 이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.
TypedQuery<UserDTO> query =
em.createQuery("SELECT new jpabook.jpql.UserDTO(m.username, m.age)
FROM Member m", UserDTO.class);
List<UserDTO> resultList = query.getResultList();
public class UserDTO {
private String username;
private int age;
public UserDTO(String username, int age) {
this.username = username;
this.age = age;
}
//..
}
다음 2가지를 주의하자.
• 패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
• 순서와 타입이 일치하는 생성자 필요하다.
📌 세 가지 방식 중 new 명령어로 조회가 제일 깔끔해서 사용하는 것을 권장!!
JPA는 페이징을 다음 두 API로 추상화했다. 데이터베이스마다 다른 페이징 처리를 같은 API로 처리할 수 있는 것은 데이터베이스 방언 덕분이다. 설정한 방언 정보에 따라 JPQL이 각각의 데이터베이스별 SQL로 변환된다.
페이징 API 예시
검색 조건 : 나이 10살
정렬 조건 : 이름으로 내림차순
페이징 조건 : 첫 번째 페이지, 페이지당 보여줄 데이터는 3건
public List<Member> findByPage(int age, int offset, int limit) {
return em.createQuery("select m from Member m where m.age = :age order by m.username desc")
.setParameter("age", age)
.setFirstResult(offset)
.setMaxResults(limit)
.getResultList();
}
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m
select
case t.name
when '팀A' then '인센티브110%'
when '팀B' then '인센티브120%'
else '인센티브105%'
end
from Team t
select coalesce(m.username,'이름 없는 회원') from Member m
select NULLIF(m.username, '관리자') from Member m
함수 | 설명 |
---|---|
COUNT | 결과 수를 구한다. |
MAX, MIN | 최대, 최소 값을 구한다. 문자, 숫자, 날짜 등에 사용한다. |
AVG | 평균값을 구한다. |
SUM | 합을 구한다. |
GROUP BY | 특정 그룹끼리 묶어준다. |
HAVING | GROUP BY와 함께 사용하는데 GROUP BY로 그룹화한 통계 데이터를 기준으로 필터링한다. |
ORDER BY | ORDER BY는 결과를 정렬할 때 사용한다. |
JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.
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
JPA 2.1 부터 조인할 때 ON 절을 지원한다. ON절을 활용하면 조인 대상 필터링과 연관관계 없는 엔티티 외부 조인을 할 수 있다.
SELECT m, t FROM Member m LEFT JOIN m.team t on t.name = 'A'
SELECT m, t FROM
Member m LEFT JOIN Team t on m.username = t.name
서브 쿼리 지원 함수
ex) 나이가 평균보다 많은 회원
select m from Member m
where m.age > (select avg(m2.age) from Member m2)
ex) 팀A 소속인 회원
select m from Member m
where exists (select t from m.team t where t.name = ‘팀A')
서브 쿼리로 해결이 안되면 조인으로 생각해보자.
경로 표현식이란 .(점)을 찍어 객체 그래프를 탐색하는 것이다.
상태 필드(state field): 단순히 값을 저장하기 위한 필드(ex: m.username)
연관 필드(association field): 연관관계를 위한 필드
@ManyToOne
, @OneToOne
, 대상이 엔티티(ex: m.team) @OneToMany
, @ManyToMany
, 대상이 컬렉션(ex: m.orders)경로 탐색의 끝, 탐색X
JPQL: select m.username, m.age from Member m
SQL: select m.username, m.age from Member m
묵시적 내부 조인(inner join) 발생, 탐색O
JPQL: select o.member from Order o
SQL: select m.* from Orders o inner join Member m on o.member_id = m.id
JPQL을 보면 o.member를 통해 주문에서 회원으로 단일 값 연관 필드로 경로 탐색을 했다. 단일 값 연관 필드로 경로 탐색을 하면 SQL에서 내부 조인이 일어나는데 이것을 묵시적 조인이라 한다. 참고로 묵시적 조인은 모두 내부 조인이다. 외부 조인은 명시적으로 JOIN 키워드를 사용해야 한다.
명시적 조인 : JOIN을 직접 적어주는 것.
ex) SELECT m FROM Member m JOIN m.team t
묵시적 조인 : 경로 표현식에 의해 묵시적으로 조인이 일어나는 것. 내부 조인 (INNER JOIN)만 할 수 있다.
ex) SELECT m.team FROM Member m
묵시적 내부 조인 발생, 탐색X
JPQL을 다루면서 많이 하는 실수 중 하나는 컬렉션 값에서 경로 탐색을 시도하는 것이다.
select t.members from Team t //성공
select t.members.username from Team t //실패
t.members처럼 컬렉션까지는 경로 탐색이 가능하지만 t.members.username처럼 컬렉션에서 경로 탐색을 시작하는 것은 허락하지 않는다. t.members는 멤버 객체가 아니라 컬렉션 그 자체이기 때문이다. 만약 컬렉션에서 경로 탐색을 하고 싶으면 명시적 조인을 사용해서 별칭을 획득해야 한다.
select m.username from Team t join t.members m
묵시적 조인은 SQL 튜닝을 어렵게 만들고 조인이 일어나는 상황을 한눈에 파악하기 어렵다. 따라서 가급적 묵시적 조인 대신에 명시적 조인을 사용하자.
TYPE은 엔티티의 상속 구조에서 조회 대상을 특정 자식 타입으로 한정할 때 주로 사용한다.
예) Item 중에 Book, Movie를 조회해라
select i from Item i
where type(i) IN (Book, Movie)
select i from i
where i.DTYPE in (‘B’, ‘M’)
Threat는 JPA 2.1에 추가된 기능인데 자바의 타입 캐스팅과 비슷하다. 상속 구조에서 부모 타입을 특정 자식 타입으로 다룰 때 사용한다. FROM, WHERE, SELECT(하이버네이트 지원)에서 사용할 수 있다.
예) 부모인 Item과 자식 Book이 있다.
select i from Item i
where treat(i as Book).auther = ‘kim’
select i.* from Item i
where i.DTYPE = ‘B’ and i.auther = ‘kim’
JPQL에서 엔티티를 직접 사용하면 SQL에서 해당 엔티티의 기본 키 값을 사용한다.
select count(m.id) from Member m //엔티티의 아이디를 사용
select count(m) from Member m //엔티티를 직접 사용
select count(m.id) as cnt from Member m
Team team = em.find(Team.class, 1L);
String qlString = “select m from Member m where m.team = :team”;
List resultList = em.createQuery(qlString)
.setParameter("team", team) //엔티티를 직접 사용
.getResultList();
String qlString = “select m from Member m where m.team.id = :teamId”;
List resultList = em.createQuery(qlString)
.setParameter("teamId", teamId) //외래 키에 식별자를 직접 사용
.getResultList();
select m.* from Member m where m.team_id=?
JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
동적 쿼리 : em.createQuery("select ...") 처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다. 런타임에 특정 조건에 따라 JPQL을 동적으로 구성할 수 있다.
정적 쿼리 : 미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데 이것을 Named 쿼리라 한다. Named 쿼리는 한 번 정의하면 변경할 수 없는 정적인 쿼리다.
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해둔다. 따라서 오류를 빨리 확인할 수 있고 사용하는 시점에는 파싱된 결과를 재사용하므로 성능상 이점도 있다. 그리고 Named 쿼리는 변하지 않는 정적 SQL이 생성되므로 데이터베이스의 조회 성능 최적화에도 도움이 된다. Named 쿼리는 @NamedQuery
어노테이션을 사용해서 자바 코드에 작성하거나 또는 XML 문서에 작성할 수 있다.
@Entity
@NamedQuery(
name = "Member.findByUsername",
query="select m from Member m where m.username = :username")
public class Member {
...
}
@NamedQuery
사용List<Member> resultList =
em.createNamedQuery("Member.findByUsername", Member.class)
.setParameter("username", "회원1")
.getResultList();
if 재고가 10개 미만인 모든 상품의 가격을 10% 상승하려면?
따라서 변경된 데이터가 100건이라면 100번의 UPDATE SQL이 실행된다...
하지만 벌크 연산은 쿼리 한 번으로 여러 테이블의 로우를 변경할 수 있다.
String qlString = "update Product p " +
"set p.price = p.price * 1.1 " +
"where p.stockAmount < :stockAmount";
int resultCount = em.createQuery(qlString)
.setParameter("stockAmount", 10)
.executeUpdate();
벌크 연산은 영속성 컨텍스트를 무시하고 데이터베이스에 직접 쿼리한다. 따라서 다음과 같은 해결책이 있다.