Criteria
- JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API
- 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고
문자 기반의 JPQL보다 동적 쿼리를 안전하게 생성할 수 있는 장점
- 반면 코드가 복잡하고 장황해서 직관적으로 이해가 힘듦
Criteria 기초
- Criteria API는 javax.persistence.crieria 패키지에 존재
- 가장 단순한 Criteria 쿼리
// Criteria 쿼리 시작
// 모든 회원 엔티티를 조회하는 단순한 JPQL
// JPQL : select m from Member m
/* 1. Criteria 쿼리 빌더
Criteria 쿼리를 생성하려면 먼저 Criteria 빌더를 얻어야 함.
Criteria 빌더는 EntityManager나 EntityManagerFactory에서 얻을 수 있음 */
CriteriaBuilder cb = em.getCriteriaBuilder();
/* 2. Criteria 생성, 반환 타입 지정
Criteria 쿼리 빌더에서 Cirteria 쿼리를 생성
이때 반환 타입을 지정할 수 있음 */
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
/* 3. FROM 절
FROM 절을 생성
반환된 값 m은 Criteria에서 사용하는 특별한 별치
m을 조회의 시작점이라는 의미로 쿼리 루트라고 함
루트 쿼리는 조회의 시작점이며 Criteria에서 사용되는 특별한 별칭으로 엔티티에만 부여할 수 있음 */
Root<Member> m = cq.from(Member.class);
/* 4. SELECT 절
SELECT 절을 생성 */
cq.select(m);
// Criteria 쿼리를 완성하고 나면 JPQL과 같이 em.createQuery(cq)에 완성한 쿼리를 넣어주기만 하면 됨
TypedQuery<Member> query = em.createQuery(cq);
List<Mebmer> members = query.getResultList();
// 검색 조건 추가
// Criteria는 검색 조건부터 정렬까지 Criteria 빌더를 사용해서 코드를 완성
/* JPQL : select m from Member m
where m.username='회원1'
order by m.age desc */
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class); // FROM
/* 검색 조건 정의 :
m은 회원 엔티티의 별칭이므로 m.get("username")은 JPQL에서 m.username과 같은 표현
cb.equal(m,get("username"), "회원1")은 JPQL에서 m.username = '회원1'과 같은 표현
이는 경로 표현식을 사용한 것 */
Predicate usernameEqual = cb.equal(m.get("username"), "회원1");
/* 정렬 조건 정의 :
cb.desc(m.get("age"))는 JPQL의 m.age desc와 같은 표현 */
javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));
/* 쿼리 생성 :
만들어둔 조건을 where, orderBy에 넣어서 원하는 쿼리를 생성 */
cq.select(m)
.where(usernameEqual) // WHERE
.orderBy(ageDesc); // ORDER BY
List<Mebmer> resultList = em.createQuery(cq).getResultList();
- 10살을 초과하는 회원을 조회하고 나이 역순으로 정렬
// 숫자 타입 검색
// select m from Member m
// where m.age > 10 order by m.age desc
Root<Member> m = cq.from(Member.class);
/* 타입 정보 필요가 필요
m.get("age")에서는 "age"의 타입 정보를 알지 못하므로 제네릭으로 반환 타입 정보를 명시
반면 String 같은 문자 타입은 지정하지 않아도 됨
또한 greaterThan() 대신에 gt()를 사용해도 됨 */
Predicate ageGt = cb.greaterThan(m.<Integer>get("age"), 10);
cq.select(m);
cq.where(ageGt);
cq.orderBy(cb.desc(m.get("age")));
Criteria 쿼리 생성
- Criteria를 사용하려면 CriteriaBuilder.createQuery() 메소드로 Criteria 쿼리(CriteriaQuery)를 생성
// CriteriaBuilder
public interface CriteriaBuilder {
CriteriaQuery<Object> createQuery(); // 조회값 반환 타입 : Object
// 조회값 반환 타입 : 엔티티, 임베디드 타입, 기타
<T> CriteriaQuery<T> createQuery(Class<T> resultClass);
CriteriaQuery<Tuple> createTupleQuery(); // 조회값 반환 타입 : Tuple
...
}
- Criteria 쿼리를 생성할 때 파라미터로 쿼리 결과에 대한 반환 타입을 지정할 수 있음
// 반환 타입 지정
CriteriaBuilde cb = em.getCriteriaBuilder();
// CriteriaQuery를 생성할 때 Member를 반환 타입으로 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
...
// 위에서 Member를 타입으로 지정했으므로 지정하지 않아도 Member 타입을 반환
List<Member> resultList = em.createQuery(cq).getResultList();
- 반환 타입을 지정할 수 없거나 반환 타입이 둘 이상이면 타입을 지정하지 않고 Object로 반환
// Object로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Obejct> cq = cb.createQuery(); // 조회값 반환 타입 : Object
...
List<Object> resultList = em.createQuery(cq).getResultList();
- 반환 타입이 둘 이상이면 Object[]를 사용하는 것이 편리
// Object[]로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Obejct[]> cq = cb.createQuery(Object[].class); // 조회값 반환 타입 : Object[]
...
List<Object[]> resultList = em.createQuery(cq).getResultList();
// 튜플로 조회
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Tuple> cq = cb.createTupleQuery(); // 조회값 반환 타입 : Tuple
...
TypedQuery<Tuple> query = em.createQuery(cq);
조회
// CriteraiQuery
public interface CriteriaQuery<T> extends AbstractQuery<T> {
// 한 건 지정
CriteriaQuery<T> select(Selection<? extends T> selection);
// 여러 건 지정
CriteriaQuery<T> multiselect(Selection<?>... selection);
// 여러 건 지정
CriteriaQuery<T> multiselect(List<Selection<?>> selection);
...
}
// select에 조회 대상을 하나만 지정
// JPQL : select m
cq.select(m)
// select에 조회 대상을 여러 건 지정
// JPQL : select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));
// 또 다른 여러 건 지정
// JPQL : select m.username, m.age
CriteriaBuilder cb = em.getCriteriaBuilder();
cq.select(cb.array(m.get("username"), m.age("age")).distinct(true);
- DISTINCT
distinct는 select, multiselect 다음에 distinct(true)를 사용
// JPQL : select distinct m.username, m.age from Member m
CriteriaQuery<Obejct[]> cq = cb.createQuery(Object[].class);
Root<Member> m = cq.from(Member.class);
cq.multiselect(m.get("username"), m.get("age")).distinct(true);
// 또는 cq.select(cb.array(m.get("username"), m.age("age")).distinct(true);
TypedQuery<Object[]> query = em.creatQuery(cq);
List<Object[]> resultList = query.getResultList();
- NEW, construct()
JPQL에서의 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스 타입, ...)로 사용
// Criteria construct()
// JPQL " select new jpabook.domain.MemberDTO(m.username, m.age) from MEmber
CriteriaQuery<MemberDTO> cq = cb.createQuery(MemberDTO.class);
Root<Member> m = cq.from(Member.class);
// JPQ에서는 패키지명을 다 적어주었으나, Criteria는 코드를 직접 다루므로 간략하게 사용 가능
cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));
TypedQuery<MemberDTO> query = em.createQuery(cq);
List<MemberDTO> resultList = query.getResultList();
- 튜플
Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공하며 튜플로 엔티티도 조회할 수 있음
// 튜플
// 튜플은 이름 기반이므로 순서 기반의 Object[] 보다 안전
// JPQL : select m.username, m.age from Member m
CriteriaBuilder cb = em.getCriteriaBuilder();
// 튜플을 사용하려면 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria 생성
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
// CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class);와 같음
Root<Member> m = cq.from(Member.class);
// 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당
cq.multiselect(
m.get("username").alias("username"), // 튜플에서 사용할 튜플 별칭
m.get("age").alias("age")
);
TypedQuery<Tuple> query = em.createQuey(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
// 선언해둔 튜플 별칭으로 데이터를 조회
// 튜플 별칭으로 조회
String username = tuple.get("username", String.class);
Integer age = tuple.get("age", Integer.class);
}
// 튜플로 엔티티 조회
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);
cq.select(cb.tuple(
m.alias("m"), // 회원 엔티티, 별칭 m
m.get("username").alias("username") // 단순 값 조회, 별칭 username
));
TypedQuery<Tuple> query = em.createQuey(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
Member member = tuple.get("m", Member.class);
String username = tuple.get("username", String.class);
}
집합
- GROUP BY
팀 이름별로 나이가 가장 많은 사람과 가장 적은 사람을 구할 때 사용
// 집합 예
/* JPQL :
select m.team.name, max(m.age), min(m.age)
from Member m
group by m.team.name
*/
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Obejct[]> cq = cb.createQuery(Object[].class);
Root<Member> m = cq.from(Member.class);
Expression maxAge = cb.max(m.<Integer>get("age"));
Expression minAge = cb.min(m.<Integer>get("age"));
cq.multiselect(m.get("team").get("name"), maxAge, minAge);
cq.groupBy(m.get("team").get("name")); // GROUP BY (JPQL의 group by m.team.name과 동일)
TypedQuery<Object[]> query = em.creatQuery(cq);
List<Object[]> resultList = query.getResultList();
- HAVING
팀에 가장 나이 어린 사람이 10살을 초과하는 팀을 조회한다는 조건을 추가
cq.multiselect(m.get("team").get("name"), maxAge, minAge)
.groupBy(m.get("team").get("name"))
.having(cb.gt(minAge), 10)); // HAVING (JPQL에서 having min(m.age) > 10 과 같음)
정렬
- 정렬 조건도 Criteria 빌더를 통해서 생성
// cb.desc(...) 또는 cb.asc(...)로 생성
cq.select(m)
.where(ageGt)
.orderBy(cb.desc(m.get("age"))); // JPQL : order by m.age desc
CriteriaQuery<T> orderBy(Order... o);
CriteriaQuery<T> orderBy(List<Order> o);
조인
- join() 메소드와 JoinType 클래스를 사용
// JoinType 클래스
public enum JoinType {
INNER, // 내부 조인
LEFT, // 왼쪽 외부 조인
RIGHT // 오른쪽 외부 조인
// JPA 구현체나 데이터베이스에 따라 지원하지 않을 수 있음
}
// JOIN 예
// 회원과 팀을 조인
/* JPQL :
select m, t from MEmber m
inner join m.team t
where t.name = '팀A' */
Root<Member> m = cq.from(Member.class);
/* 쿼리 루트(m)에서 m.join("team") 메소드를 사용해서
회원과 팀을 JoinType.INNER를 설정해서 내부 조인한 후
조인한 team에 t라는 별칭을 줌
조인 타입을 생략하면 내부 조인을 사용 */
Join<Member, Team> t = m.join("team", JoinType.INNER); // 내부 조인
cq.multiselect(m, t)
.where(cb.equal(t.age("name"), "팀A"));
- 페치 조인
페치 조인은 fetch(조인대상, JoinType)을 사용
Root<Member> m = cq.from(Member.class);
m.fetch("team", JoinType.LEFT);
cq.select(m);
서브 쿼리
- 간단한 서브 쿼리
메인 쿼리와 서브 쿼리 간에 관련이 없는 단순한 서브 쿼리
// 간단한 서브 쿼리
// 평균 나이 이상의 회원을 구하는 서브 쿼리
/* JPQL :
select m from Member m
where m.age >= (select AVG(m2.age) from Member m2) */
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
// 서브 쿼리 생성
SubQuery<Double> subQuery = mainQuery.subQuery(Double.class);
Root<Member> m2 = subQuery.from(Member.class);
subQuery.select(cb.avg(m2.<Integer>get("age")));
// 메인 쿼리 생성
Root<Member> m = mainQuery.from(Member.class);
mainQuery.select(m)
.where(cb.ge(m.<Integer>get("age"), subQuery); // 생성한 서브 쿼리를 사용
- 상호 관련 서브 쿼리
메인 쿼리와 서브 쿼리 간에 서로 관련이 있을 때
서브 쿼리에서 메인 쿼리의 정보를 사용하려면 메인 쿼리에서 사용한 별칭을 얻어야하며
서브 쿼리는 메인 쿼리의 Root나 Join을 통해 생성된 별칭을 받아서 사용
// 상호 관련 서브 쿼리
// 팀A에 소속된 회원 찾기
/* JPQL :
select m from Member m
where exists (select t from m.team t where t.name='팀A') */
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> mainQuery = cb.createQuery(Member.class);
// 서브 쿼리에서 사용되는 메인 쿼리의 m
Root<Member> m = mainQuery.from(Member.class);
// 서브 쿼리 생성
// correlate(...) 메소드를 사용해 메인 쿼리의 별칭을 서브 쿼리에서 사용
SubQuery<Team> subQuery = mainQuery.subQuery(Team.class);
Root<Member> subM = subQuery.correlate(m); // 메인 쿼리의 별칭을 가져옴
Join<Member, Team> t = subM.join("team");
subQuery.select(t)
.where(cb.equal(t.get("name"), "팀A"));
// 메인 쿼리 생성
mainQuery.select(m)
.where(cb.exists(subQuery));
List<Member> resultList = em.createQuery(mainQuery).getResultList();
IN 식
- Criteria 빌더에서 in(...) 메소드를 사용
// IN 식 사용 예
/* JPQL :
select m from Member m
where m.username in ("회원1", "회원2") */
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);
cq.select(m)
.where(cb.in(m.get("username"))
.value("회원1")
.value("회원2"));
CASE 식
- selectCase() 메소드와 when(), otherwise() 메소드를 사용
// CASE 식 사용 예
/* JPQL :
select
case when m.age <= 10 then '학생요금'
when m.age >= 60 then '경로요금'
else '일반요금'
end
from Member m */
Root<Member> m = cq.from(Member.class);
cq.multiselect(
m.get("username"),
cb.selectCase()
.when(cb.ge(m.<Integer>get("age"), 60), 600)
.when(cb.le(m.<Integer>get("age"), 15), 500)
.otherwise(1000)
);
파라미터 정의
- JPQL에서 :PARAM1처럼 파라미터를 정의했듯이 Criteria도 파라미터 정의 가능
// 파라미터 정의 예
/* JPQL :
select m from Member m
where m.username = :usernameParam */
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);
// cb.parameter(타입, 파라미터 이름) 메소드를 사용해서 파라미터를 정의
cq.select(m)
.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParma")));
List<Member> resultList = em.createQuery(cq)
.setParameter("usernameParam"), "회원1") // 해당 파라미터에 사용할 값을 바인딩
.getResultList();
네이티브 함수 호출
- 네이티브 SQL 함수를 호출하려면 cb.function(...) 메소드를 사용
// 전체 회원의 나이 합
// 하이버네이트 구현체는 방언에 사용자정의 SQL 함수를 등록해야 호출 가능
// SUM 대신 원하는 네이티브 SQL 함수를 입력
Root<Member> m = cq.from(Member.class);
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);
동적 쿼리
- 다양한 검색 조건에 따라 실행 시점에 쿼리를 생성하는 것을 동적 쿼리라고 함
동적 쿼리는 문자 기반인 JPQL보다는 코드 기반인 Criteria로 작성하는 것이 더 편리
- JPQL로 만든 동적 쿼리
// JPQL 동적 쿼리
// 문자 기반인 JPQL로 인해 동적 쿼리를 작성하면서 문자 더하기로 인해 여러 번 버그를 만날 것임
// 또는 where와 and의 위치를 구성하는 것에도 신경을 써야 함
// 나이, 이름, 팀명을 검색 조건으로 사용해서 동적으로 쿼리를 생성
// 검색 조건
Integer age = 10;
String username = null;
String teamName = "팀A";
// JPQL 동적 쿼리 생성
StringBuilder jpql = new StringBuilder("select m from Member m join m.team t ");
List<String> criteria = new ArrayList<String>();
if (age != null) criteria.add(" m.age = :age ");
if (username != null) criteria.add(" m.username = :username ");
if (teamName != null) criteria.add(" m.teamName = :teamName ");
if (criteria.size() > 0) jpql.append(" where ");
for (int i = 0; i < criteria.size(); i++) {
if (i > 0) jpql.append(" and ");
jpql.append(criteria.get(i));
}
TypedQuery<Member> query = em.createQuery(jpql.toString(), Member.class);
if (age != null) query.setParameter("age", age);
if (username != null) query.setParameter("username", username);
if (teamName != null) query.setParameter("teamName", teamName);
List<Member> resultList = query.getResultList();
// Criteria 동적 쿼리
// 최소한 공백이나 where, and의 위치로 인해 에러가 발생하지 않음
// 하지만 장황하고 복잡함으로 인해, 코드가 읽기 힘들다는 단점
// 나이, 이름, 팀명을 검색 조건으로 사용해서 동적으로 쿼리를 생성
// 검색 조건
Integer age = 10;
String username = null;
String teamName = "팀A";
// Criteria 동적 쿼리 생성
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team");
List<Predicate> criteria = new ArrayList<Predicate>();
if (age != null) criteria.add(cb.equal(m.<Integer>get("age"), cb.parameter(Integer.class, "age")));
if (username != null) criteria.add(cb.equal(m.get("username"), cb.parameter(String.class, "username")));
if (teamName != null) criteria.add(cb.equal(t.get("teamName"), cb.parameter(String.class, "teamName")));
cb.where(cb.and(criteria.toArray(new Predicate[0])));
TypedQuery<Member> query = em.createQuery(cq);
if (age != null) query.setParameter("age", age);
if (username != null) query.setParameter("username", username);
if (teamName != null) query.setParameter("teamName", teamName);
List<Member> resultList = query.getResultList();
함수 정리
- Criteria는 JPQL 빌더 역할을 하므로 JPQL 함수를 코드로 지원하며
JPQL에서 사용하는 함수는 대부분 CriteriaBuilder에 정의되어 있음
- Experssion의 메소드
함수명 |
JPQL |
isNull |
IS NULL |
isNotNull() |
IS NOT NULL |
in() |
IN |
함수명 |
JPQL |
and() |
and |
or() |
or |
not() |
not |
equal(), notEqual() |
=, <> |
lt(), lessThan() |
< |
le(), lessThanOrEqualTo() |
<= |
gt(), greaterThan() |
> |
ge(), greaterThanOrEqualTo() |
>= |
between() |
between |
like(), notLike() |
like, not like |
isTrue(), isFalse() |
is true, is false |
in(), not(in()) |
in, not(int()) |
exists(), not(exist()) |
exists, not exists |
isNull(), isNotNull() |
is null, is not null |
isEmpty(), isNotEmpty() |
is empty, is not empty |
isMember(), isNotMember() |
member of, not member of |
함수명 |
JPQL |
sum() |
+ |
neg(), diff() |
- |
prod() |
* |
quot() |
/ |
all() |
all |
any() |
any |
some() |
some |
abs() |
abs |
sqrt() |
sqrt |
mod() |
mod |
size() |
size |
length() |
length |
locate() |
locate |
concat() |
concat |
upper() |
upper |
lower() |
lower |
substring() |
substring |
trim() |
trim |
currentDate() |
current_date |
currentTime() |
current_time |
currentTimestamp() |
current_timestamp |
함수명 |
JPQL |
avg() |
avg |
max(), greatest() |
max |
min(), least() |
min |
sum(), sumAsLong(), sumAsDouble() |
sum |
count() |
count |
countDistinct() |
count distinct |
함수명 |
JPQL |
nullif() |
nullif |
coalesce() |
coalesce |
selectCase() |
case |
Criteria 메타 모델 API
- Criteria는 코드 기반이므로 컴파일 시점에 오류를 발생할 수 있지만,
m.get("age")에서 처럼 age는 문자이므로 잘못 적어도 컴파일 시점에 에러를 발견하지 못하므로
완전한 코드 기반으로 작성하려면 메타 모델 API를 사용
// 메타 모델 API 적용 전 (문자 기반)
cq.select(m)
.where(cb.gt(m.<Integer>get("age"), 20))
.orderBy(cb.desc(m.get("age")));
// 메타 모델 API 적용 후 (정적인 코드 기반)
// 이를 위해서 메타 모델 클래스인 Member_ 클래스 필요
cq.select(m)
.where(cb.gt(m.get(Member_.age), 20))
.orderBy(cb.desc(m.get(Member_.age)));
// Member_ 클래스
// 표준 메타 모델 클래스 (메타 모델)
// Member_ 메타 모델 클래스는 Member 엔티티를 기반으로 만들어야 함
// 코드 자동 생성기가 엔티티 클래스를 기반으로 메타 모델 클래스들을 만들어 줌
// 코드 생성기는 모든 엔티티 클래스를 찾아서 "엔티티명_.java" 모양의 메타 모델 클래스를 생성
/* 즉, 엔티티 -> 코드 자동 생성기 -> 메타 모델 클래스
src/jpabook/domain/Member.java // 원본 코드
target/generated-srouces/annotations/jpabook/domin/Member_.java // 자동 생성된 메타 모델 */
@Generated(value = "org.hibernate.jpamodelgen.JPAMetaModelEntityProcessor") // 코드 생성기
@StaticMetamodel(Member.class)
public abstract class Member_ {
public static volatile SingularAttribute<Member, Long> id;
public static volatile SingularAttribute<Member, String> username;
public static volatile SingularAttribute<Member, Integer> age;
public static volatile ListAttribute<Member, Order> orders;
public static volatile SingularAttribute<Member, Team> team;
}
- 코드 생성기 설정
코드 생성기는 보통 메이븐이나 엔트, 그래들 같은 빌드 도구를 사용해서 실행
// 코드 생성기 MAVEN 설정
<dependencies>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hubernate-jpabodelgen</artifactId>
<version>1.3.0.Final</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compileArguments>
<processor>org.hibernate.jpamodelgen,JPAMetaModelEntityProcessor</process>
</complieArguments>
</configuration>
</plugin>
</plugins>
</build>
// 이후 mvn compile 명령을 사용하면 target/generated-srouces/annotations/ 하위에 메타 모델 클래스들이 생성됨