EntityManager.find() 메소드를 사용해 식별자로 엔티티를 조회할 수 있고, 조회한 엔티티에 객체 그래프 탐색을 사용해 연관된 엔티티들을 찾을 수 있다.
이 둘은 가장 단순한 검색 방법이다.
조금 더 복잡한 검색 방법이 필요한 경우는 어떻게 할까?
가령 나이가 30살 이상인 회원을 모두 검색하고 싶다면?
모든 회원 엔티티를 메모리에 올려두고 애플리케이션에서 30살 이상인 회원을 검색하는 것은 현실성이 없을 것이다.
ORM은 데이터베이스 테이블이 아닌 엔티티 객체를 대상으로 개발하므로, 검색도 테이블이 아닌 엔티티 객체를 대상으로 하는 방법이 필요할 것이다.
위 문제를 해결하기 위해 만들어진 것이 JPQL(Java Persistence Query Language)이다.
JPQL에는 다음과 같은 특징이 있다.
테이블이 아닌 객체를 대상으로 검색하는 객체지향 쿼리이다.
SQL을 추상화해서 특정 데이터베이스 SQL에 의존하지 않는다.
쉽게 말하면, SQL이 데이터베이스 테이블을 대상으로 하는 데이터 중심의 쿼리라면, JPQL은 엔티티 객체를 대상으로 하는 객체지향 쿼리이다.
JPQL
JPQL은 SQL을 추상화해서 특정 데이터베이스에 의존하지 않는다.
JPQL은 SQL보다 간결하다.
예시
회원 엔티티를 대상으로 JPQL을 사용하는 간단한 예시이다.
실행한 JPQL 대비, 실행된 SQL을 참고하면 보다 간단한 것을 알 수 있다.
@Entity(name = "Member")
public class Member {
@Column(name = "name")
private String username;
...
}
String jpql = "select m from Member as m where m.username = 'kim'";
List<Member> resultList = em.createQuery(jpql, Member.class).getResultList();
# 실행한 JPQL
select m
from Member as m
where m.username = 'kim'
# 실제 실행된 SQL
select
member.id as id,
member.age as age,
member.team_id as team,
member.name as name,
from
Member member
where
member.name='kim'
JPQL 사용 방법
JPQL도 SQL과 비슷하게 SELECT, UPDATE, DELETE 문을 사용할 수 있다.
엔티티를 저장할 때는 EntityManager.persist()를 사용하면 되기 때문에 INSERT 문은 없다.
SELECT 문
대소문자 구분
엔티티와 속성은 대소문자를 구분한다.
그러나 SELECT, FROM, AS 등과 같은 JPQL 키워드는 대소문자를 구분하지 않는다.
엔티티 이름
JPQL에서 사용한 Member는 클래스 명이 아니라 엔티티 명이다.
엔티티명은 @Entity(name="something")으로 지정할 수 있다.
별칭은 필수
Member AS m 에서 m은 별칭이다.
만약 별칭 없이 작성하면 잘못된 문법이라는 오류가 발생한다.
SELECT username FROM Member m: m.username으로 고쳐야한다.
AS는 생략할 수 있다.
TypeQuery, Query
작성할 JPQL을 실행하려면 쿼리 객체를 만들어야 한다.
반환할 타입을 명확하게 지정할 수 있으면 TypeQuery 사용
반환 타입을 명확하게 지정할 수 없으면 Query 사용
TypedQuery<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);
}
Query<Member> query = em.createQuery("SELECT m FROM Member m", Member.class);
List resultList = query.getResultList();
for (Object o : resultList) {
Object[] result = (Object[]) o;
System.out.println("username = " + result[0]);
System.out.println("age = " + result[1]);
}
파라미터 바인딩
JDBC는 위치 기준 파라미터 바인딩만 지원하지만, JPQL은 이름 기준 파라미터 바인딩도 지원한다.
이름 기준 파라미터
이름 기준 파라미터는 파라미터를 이름으로 구분하는 방법이다.
앞에 :를 사용한다.
예시
query.setParameter()에서 파라미터 바인딩이 가능하다.
참고로 JPQL API는 대부분 메소드 체인 방식으로 설계되어 있어 연속해서 작성할 수 있다.
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();
// 체인 방식으로 작성
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();
파라미터 바인딩 주의사항
// 파라미터 바인딩 방식을 사용하지 않고 직접 JPQL을 만들어 사용하는 경우
”select m Member m where m.username = ’” + usernameParam + ”’”
JPQL을 수정해서 파라미터 바인딩 방식이 아닌, 직접 문자열을 더해 만들면 악의적인 사용자에 의해 SQL 인젝션 공격을 당할 수 있다.
성능 이슈 또한 발생할 수 있는데, 파라미터 바인딩 방식을 사용하면 파라미터의 값이 달라도 같은 쿼리로 인식해서 JPA는 JPQL을 SQL로 파싱한 결과를 재사용할 수 있다.
파라미터 바인딩 방식은 선택이 아닌 필수이다.
프로젝션
SELECT 절에 조회할 대상을 지정하는 것을 프로젝션이라 한다.
[SELECT {프로젝션 대상} FROM]으로 대상을 선택한다.
프로젝션 대상은 엔티티, 엠비디드 타입, 스칼라 타입이 있다.
엔티티 프로젝션
SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
이렇게 조회한 엔티티는 영속성 컨텍스트에서 관리된다.
임베디드 타입 프로젝션
JPQL에서 임베디드 타입은 엔티티와 거의 비슷하게 사용된다.
그러나 임베디드 타입은 조회의 시작점이 될 수 없다는 제약이 있다.
임베디드 타입은 엔티티 타입이 아닌 값 타입이다.
직접 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않는다.
스칼라 타입 프로젝션
통계 쿼리 등을 주로 스칼라 타입으로 조회한다.
NEW 명령어
만약 String username, int age 두 필드를 프로젝션하려면 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.
반환 값으로는 Object[]를 반환받게 된다.
이런 반환 값을 DTO로 매핑해 사용해야 한다.
이런 객체 변환 작업을 NEW를 사용해 해결할 수 있다.
SELECT 다음에 NEW 명령어를 사용하면 반환받을 클래스를 지정할 수 있다.
"SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m"
이 클래스의 생성자에 JPQL 조회 결과를 넘겨줄 수 있다.
NEW 명령어를 사용한 클래스로 TypeQuery를 사용할 수 있어 객체 변환 작업을 줄일 수 있다.
NEW 명령어 사용 시, 2 가지 주의 사항이 있다.
패키지 명을 포함한 전체 클래스 명을 입력해야 한다.
순서와 타입이 일치하는 생성자가 필요하다.
페이징 API
데이터베이스 마다 페이징을 처리하는 SQL 문법은 다르다.
JPA는 페이징을 다음 두 API로 추상화했다.
setFirstResult(int startPosition): 조회 시작 위치 (0부터 시작한다)
setMaxResults(int maxResult): 조회할 데이터 수
집합과 정렬
집합은 집합함수와 함께 통계 정보를 구할 때 사용한다.
집합 함수 사용 시 참고사항
NULL 값은 무시하고 통계에 잡히지 않는다. (DISTINCT가 정의 되어 있어도 무시된다.)
만약 값이 없는데 SUM, AVG, MAX, MIN 함수를 사용하면 NULL 값이 된다.
단 COUNT는 0이 된다.
DISTINCT를 집합 함수 안에 사용해서 중복된 값을 제거하고나서 집합을 구할 수 있다.
예: select COUNT ( DISTINCT m.age ) from Member m
DISTINCT를 COUNT에서 사용할 때 임베디드 타입은 지원하지 않는다.
JPQL 조인
JPQL도 조인을 지원하는데 SQL 조인과 기능은 같고 문법만 약간 다르다.
내부 조인
내부 조인은 INNER JOIN을 사용한다.
INNER는 생략할 수 있다.
외부 조인
외부 조인은 기능상 SQL의 외부 조인과 같다.
OUTER는 생략 가능해서 보통 LEFT JOIN으로 사용한다.
컬렉션 조인
일대다 관계나 다대다 관계처럼 컬렉션을 사용하는 곳에 조인하는 것을 컬렉션 조인이라 한다.
세타 조인
WHERE 절을 사용해 세타 조인을 할 수도 있다.
세타 조인은 내부 조인만 지원한다.
세타 조인을 사용하면 전혀 관계 없는 엔티티도 조인할 수 있다.
전혀 관련없는 Member.username과 Team.name을 조인할 수 있다.
# JPQL
select count (m) from Member m, Team t
where m.username = t.name
# SQL
SELECT COUNT(M.ID)
FROM
MEMBER M CROSS JOIN TEAM T
WERHE
M.USERNAME=T.NAME
JOIN ON 절 (JPA 2.1)
JPA 2.1부터 조인할 때 ON 절을 지원한다.
ON 절을 사용하면 조인 대상을 필터링하고 조인할 수 있다.
내부 조인의 ON 절은 WHERE 절을 사용할 때와 결과가 같으므로 보통 ON 절은 외부 조인에서만 사용한다.
페치 조인
페치 조인은 SQL에서 이야기하는 조인의 종류는 아니고 JPQL에서 성능 최적화를 위해 제공하는 기능이다.
페치 조인을 사용해 회원 엔티티를 조회하며 연관된 팀 엔티티도 함께 조회하는 JPQL은 아래와 같다.
select m from Member m join fetch m.team
일반적인 JPQL 조인과는 다르게 m.team다음에 별칭이 없는데, 페치 조인은 별칭을 사용할 수 없다.
페치 조인과 일반 조인의 차이
페치 조인을 사용하지 않고 조인만 사용한다면 어떻게 될까?
JPQL은 결과를 반환할 때 연관관계까지 고려하지 않는다.
단지 SELECT 절에 지정한 엔티티만 조회할 뿐이다.
지연 로딩, 즉시 로딩 설정에 따라 빈 객체를 반환하거나 쿼리를 한번 더 실행하게 된다.
페치 조인의 특징
페치 조인을 사용하면 SQL 한 번으로 연관된 엔티티들을 함께 조회할 수 있어서 SQL 호출 횟수를 줄여 성능을 최적화할 수 있다.
엔티티에 직접 적용하는 로딩 전략은 애플리케이션 전체에 영향을 미치므로 글로벌 로딩 전략이라고 부른다.
페치 조인은 글로벌 로딩 전략보다 우선한다.
즉, 글로벌 로딩 전략을 지연 로딩으로 설정해도 JPQL에서 페치 조인을 사용하면 페치 조인을 적용해서 함께 조회한다.
가급적 글로벌 로딩 전략은 지연 로딩을 사용하고 최적화가 필요할 때 페치 조인을 적용하는 것이 효과적이다.
페치 조인의 한계
페치 조인 대상에는 별칭을 줄 수 없다.
따라서 SELECT, WEHRE 절, 서브 쿼리에 페치 조인 대상을 사용할 수 없다.
둘 이상의 컬렉션을 페치할 수 없다.
컬렉션을 페치 조인하면 페이징 API를 사용할 수 없다.
서브 쿼리
JPQL도 SQL처럼 서브 쿼리를 지원한다.
서브 쿼리는 WHERE, HAVING 절에서만 사용할 수 있다.
서브 쿼리는 다음 함수들과 같이 사용할 수 있다.
EXISTS, {ALL | ANY | SOME}, IN
Named 쿼리: 정적 쿼리
JPQL 쿼리는 크게 동적 쿼리와 정적 쿼리로 나눌 수 있다.
동적 쿼리
em.createQuery("select . . .")처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다.
정적 쿼리
미리 정의한 쿼리에 이름을 부여해서 필요할 때 사용할 수 있는데, 이를 Named 쿼리라 한다.
Named 쿼리는 애플리케이션 로딩 시점에 JPQL 문법을 체크하고 미리 파싱해 두기 때문에 오류를 빨리 확인할 수 있고, 사용하는 시점에 파싱된 결과를 재사용하는 것이므로 성능상 이점도 있다.
Named 쿼리를 애노테이션에 정의
@NamedQuery 애노테이션을 클래스에 정의해 사용할 수 있다.
만약 엔티티 2개 이상의 Named 쿼리를 정의하려면 @NamedQueries 애노테이션을 사용하면 된다.
Criteria
Criteria는 JPQL을 생성하는 빌더 클래스이다.
Criteria의 장점은 문자가 아닌 query.select(m).where(...) 처럼 프로그래밍 코드로 JPQL을 작성할 수 있다는 점이다.
JPQL을 String 값으로 넘길 경우, 오타에 의한 오류는 컴파일 시점에 알 수 없게 된다.
JPQL을 String 값으로 사용할 경우, 동작 쿼리 또한 작성이 까다로워진다.
예시
Criteria의 아쉬운 점을 꼽자면, 필드 명을 문자열로 작성해야하는 점이다.
이 부분을 개선하기 위한 방법으로는 메타 모델 API를 사용하는 방법이 있다.
메타 모델 사용 전, 후 예시
m.get("username")
m.get(Member_.username)
Criteria가 가진 장점은 많지만, 모든 장점을 상쇄 만큼 복잡하고 장황하다는 단점이 있다.
//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();
QueryDSL
QueryDSL도 Criteria처럼 JPQL 빌더 역할을 한다.
QueryDSL의 장점은 코드 기반이면서 단순하고 사용하기 쉽다.
작성한 코드 또한 JPQL과 비슷해서 한눈에 들어온다.
QueryDSL은 JPA 표준은 아니고 오픈소스 프로젝트다.
QueryDSL은 JPA 뿐만 아니라 JDO, 몽고DB, Java Collection, Hibernate Search 등도 거의 같은 문법으로 지원한다.
예시
쿼리 전용 클래스를 사용해야 한다.
특별한 설명을 하지 않아도 코드만으로 쿼리를 이해할 수 있다.
//준비
JPAQuery query = new JPAQuery(em);
QMember member = QMember.member;
//쿼리, 결과 조회
List<Member> members = query.from(member)
.where(member.username.eq("kim"))
.list(member);
QueryDSL 사용 방법
QueryDSL의 사용은 다음과 같다.
EntityManager em = emf.createEntityManager();
JPAQuery query = new JPAQuery(em);
QMember qMember = new QMember("m"); // 생성되는 JPQL의 별칭이 m
List<Member> members = query.from(qMember)
.where(qMember.name.eq("회원1"))
.orderBy(qMember.name.desc())
.list(qMember);
기본 Q 생성
쿼리 타입(Q)은 사용하기 편리하도록 기본 인스턴스를 보관하고 있다.
별칭을 직접 지정해서 사용해야 한다.
public class QMember extends EntityPathBase<Member> {
public static final QMember member = new QMember("member1");
...
}
검색 조건 쿼리
QueryDSL의 where 절에는 and나 or를 사용할 수 있다.
따라서 여러 검색 조건을 사용해도 된다.
동적 쿼리
com.mysema.query.BooleanBuilder를 사용하면 특정 조건에 따른 동적 쿼리를 편리하게 생성할 수 있다.
SearchParam param = new SearchParam();
param.setName("둘리");
param.setPrice(10000);
QItem item = QItem.item;
BooleanBuilder builder = new BooleanBuilder();
if (StringUtils.hasText(param.getName())) {
builder.and(item.name.contains(param.getName()));
}
if (param.getPrice() != null) {
builder.and(item.price.gt(param.getPrice()));
}
List<Item> result = query.from(item)
.where(builder)
.list(item);
메소드 위임
메소드 위임 기능을 사용하면 쿼리 타입에 검색 조건을 직접 정의할 수 있다.
조건을 정의하고 사용하는 방법은 아래와 같다.
public class ItemExpression {
@QueryDelegate(Item.class)
public static BooleanExpression isExpensive(QItem item, Integer price) {
return item.price.gt(price);
}
}
public class QItem extends EntityPathBase<Item> {
...
public BooleanExpression isExpensive(Integer price) {
return ItemExpression.isExpensive(this, price);
}
}
네이티브 SQL
JPA는 SQL을 직접 사용할 수 있는 기능을 지원하는 데 이를 네이티브 SQL이라 한다.
JPQL을 사용해도 가끔 특정 데이터베이스에 의존하는 기능을 사용해야할 때가 있는데, 이런 기능들은 전혀 표준화되어 있지 않기 때문에 JPQL에서 사용할 수 없다.
10장. 객체지향 쿼리 언어
객체지향 쿼리 소개
JPQL
(Java Persistence Query Language)이다.JPQL
예시
JPQL 사용 방법
EntityManager.persist()
를 사용하면 되기 때문에 INSERT 문은 없다.SELECT 문
@Entity(name="something")
으로 지정할 수 있다.Member AS m
에서 m은 별칭이다.SELECT username FROM Member m
: m.username으로 고쳐야한다.TypeQuery, Query
TypeQuery
사용Query
사용파라미터 바인딩
이름 기준 파라미터
:
를 사용한다.예시
query.setParameter()
에서 파라미터 바인딩이 가능하다.위치 기준 파라미터
?
다음에 위치 값을 주면 된다.파라미터 바인딩 주의사항
프로젝션
[SELECT {프로젝션 대상} FROM]
으로 대상을 선택한다.SELECT m FROM Member m // 회원
SELECT m.team FROM Member m // 팀
String username, int age
두 필드를 프로젝션하려면 TypeQuery를 사용할 수 없고 Query를 사용해야 한다.Object[]
를 반환받게 된다."SELECT new jpabook.jpql.UserDTO(m.username, m.age) FROM Member m"
페이징 API
setFirstResult(int startPosition)
: 조회 시작 위치 (0부터 시작한다)setMaxResults(int maxResult)
: 조회할 데이터 수집합과 정렬
SUM, AVG, MAX, MIN
함수를 사용하면 NULL 값이 된다.select COUNT ( DISTINCT m.age ) from Member m
JPQL 조인
내부 조인
INNER JOIN
을 사용한다.INNER
는 생략할 수 있다.외부 조인
OUTER
는 생략 가능해서 보통LEFT JOIN
으로 사용한다.컬렉션 조인
세타 조인
JOIN ON 절 (JPA 2.1)
페치 조인
join fetch
명령어로 사용할 수 있다.엔티티 페치 조인
select m from Member m join fetch m.team
m.team
다음에 별칭이 없는데, 페치 조인은 별칭을 사용할 수 없다.페치 조인과 일반 조인의 차이
페치 조인의 특징
페치 조인의 한계
서브 쿼리
Named 쿼리: 정적 쿼리
em.createQuery("select . . .")
처럼 JPQL을 문자로 완성해서 직접 넘기는 것을 동적 쿼리라 한다.@NamedQuery
애노테이션을 클래스에 정의해 사용할 수 있다.@NamedQueries
애노테이션을 사용하면 된다.Criteria
예시
m.get("username")
m.get(Member_.username)
QueryDSL
예시
QueryDSL 사용 방법
기본 Q 생성
검색 조건 쿼리
and
나or
를 사용할 수 있다.동적 쿼리
메소드 위임
네이티브 SQL