[JPA] Criteria

HyeBin, Park·2022년 6월 29일
0
post-thumbnail

JPQL을 자바 코드로 작성하도록 도와주는 빌더 클래스 API다. 문자가 아닌 코드로 JPQL을 작성하므로 문법 오류를 컴파일 단계에서 잡을 수 있고 동적 쿼리를 안전하게 생성할 수 있다는 장점이있다.
코드가 복잡하고 장황해서 직관적으로 이해가 힘들다는 단점도 있다.

🐯 Criteria

javax.persistence.criteria 패키지에 있다.

//JPQL : select m from Member m

CriteriaBuilder cb = em.getCriteriaBuilder(); //Criteria 쿼리 빌더

//Criteria 생성, 반환 타입 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);

Root<Member> m = cq.from(Member.class); //FROM 절
cq.select(m); // SELECT 절

// 완성된 쿼리를 넣어주면 된다.
TypeQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();
  • criteria 쿼리를 생성하려면 먼저 criteria 빌더를 얻어야한다. 빌더는 EntityManager나 EntityManagerFactory에서 얻을 수 있다.
  • Criteria 쿼리 빌더에서 Criteria 쿼리를 생성한다. 이때 반환 타입을 지정할 수 있다.
  • FROM 절을 생성하며 반환된 값 m은 Criteria에서 사용하는 특별한 별칭이다. m을 조회의 시작점이라는 의미로 쿼리루트라 한다.
  • SELECT 절을 생성한다.

🦁 검색 조건과 정렬 추가하기

//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 절 생성

// 검색 조건 정의
Predicate usernameEqual = cb.equal(m.get("username"), '회원1');

//정렬 조건 정의
javax.persistence.criteria.Order ageDesc = cb.desc(m.get("age"));

//쿼리 생성
cq.select(m)
	.where(usernameEqual) //WHERE 절 생성
    .orderBy(ageDesc); //ORDER BY 절 생성
    
List<Member> resultList = em.createQuery(cq).getResultList();
  • 검색 조건을 정의한 부분을 보면 m.get("username") 으로 되어 있는데 m은 회원 엔티티의 별칭이다. 이것은 JPQL에서 m.username과 같은 표현이다. 그리고 cb.equal(A,B)는 이름 그래도 A = B라는 뜻이다. 따라서 cb.equal(m.get("username"), "회원1")는 JPQL에서 m.username='회원1'과 같은 표현이다.
  • 정렬 조건을 정의하는 코드인 cb.desc(m.get("age"))는 JPQL의 m.age desc와 같은 표현이다.
  • 만들어둔 조건을 where, orderBy에 넣어서 원하는 쿼리를 생성한다.

쿼리 루트와 별칭

  • Root<Member> m = cq.from(Member.class); 여기서 m이 쿼리루트
  • 쿼리 루트트 조회의 시작점이다.
  • Criteria에서 사용되는 특별한 별칭으로 엔티티에만 부여할 수 있다.
  • 경로 표현식이 존재한다.
    • m.get("username")은 JPQL의 m.username과 같다.
    • m.get("team").get("name")은 JPQL의 m.team.name과 같다.

10살을 초과하는 회원을 조회하고 나이 역순으로 정렬

//select m from Member m
//where m.age > 10 order by m.age desc

Root<Member> m = cq.from(Member.class);

//타입 정보 필요
Predicate ageGt = cb.greaterThan(m.<Integer>get("age"), 10);

cq.select(m);
cq.where(ageGt);
cq.orderBy(cb.desc(m.get("age")));
  • m.get("age")에서는 "age"의 타입 정보를 알지 못해서 제네릭으로 반환 타입 정보를 명시해야한다.(String과 같은 문자 타입은 지정하지 않아도 된다.)
  • greaterThan() 대신에 gt()를 사용해도 된다.

Criteria 쿼리 생성

public interface CriteriaBuilder {
	CriteriaQuery<Object> createQuery(); //조회값 반환 타입 : Object
    
    //조회값 반환 타입 : 엔티티, 임베디드 타입, 기타
    <T> CriteriaQuery<T> createQuery(Class<T> resultClass);
	CriteriaQuery<Tuple> createTupleQuery(); //조회값 반환 타입: Tuple
}
  • Criteria를 사용하려면 CriteriaBuilder.createQuery() 메서드로 Criteria 쿼리를 생성하면 된다.

반환 타입

CirteriaBuilder cb = em.getCriteriaBuilder();

//Member를 반환 타입으로 지정 
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
...
//위에서 Member를 타입으로 지정했으므로 지정하지 않아도 Member 타입을 반환
List<Member> resultList = em.createQuery(cq).getResultList();
  • Criteria 쿼리를 생성할 때 파라미터로 쿼리 결과에 대한 반환 타입을 지정할 수 있다.
  • CriteriaQuery를 생성할 때 Member.class를 반환 타입으로 지정하면 em.createQuery(cq)에서 반환 타입을 지정하지 않아도 된다.

반환 타입을 지정할 수 없다면?

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> cq = cb.createQuery(); // 조회값 반환 타입 : Object
...
List<Object> resultList = em.createQuery(cq).getResultList();
  • 반환 타입을 지정할 수 없거나 반환 타입이 둘 이상이면 타입을 지정하지 않고 Object로 반환받으면 된다.

반환 타입이 둘 이상이면?

Criteriabuilder cb = em.getCriteriaBuilder();

//조회값 반환 타입 : Object[]
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
...
List<Object[]> resultList = em.createQuery(cq).getResultList();
  • 반환 타입이 둘 이상이면 Object[]를 사용하는 것이 편리하다.

반환 타입을 튜플로 받고 싶으면?

CriteriaBuilder cb = em.getCriteriaBuilder();
//조회값 반환 타입 : Tuple
CriteriaQuery<Tuple> cq = cb.createTupleQuery();
...
TypedQuery<Tuple> query = em.createQuery(cq);

튜플?

  • Criteria에서 제공하는 Map과 비슷한 반환 객체
//JPQL: select m.username, m.age from Member m
CriteriaBuilder cb = em.getCriteriabuilder();
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.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
	String username = tuple.get("username", String.class);
    
    Integer age = tuple.get("age", Integer.class);
}
  • 튜플을 사용하려면 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria 를 생성한다.
  • 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 하며 alias() 메서드로 사용해서 지정할 수 있다.
  • 선언해둔 튜플 별칭으로 데이터를 조회할 수 있다.
  • 튜플은 이름 기반이라 순서 기반의 Object[]보다 안전하고 tuple.getElements()같은 메서드를 사용해서 현재 튜플의 별칭과 자바 타입도 조회할 수 있다.

튜플로 엔티티 조회하기

CriteriaQuery<Tuple> cq = cb.createTupleQuery();
Root<Member> m = cq.from(Member.class);
cq.select(cb.tuple(
	m.alias("m"),
    m.get("username").alias("username")
));

TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultlist) {
	Member member = tuple.get("m", Member.class);
    String username = tuple.get("username", String.class);
}
  • cq.multiselect(...) 대신에 cq.select(cb.tuple(...)) 를 사용할 수 있다.

조회 (SELECT 절)

조회 대상을 한 건, 여러 건 지정

cq.select(m) // JPQL : select m
  • select 조회 대상을 하나만 지정
sq.multiselect(m.get("username"), me.get("age"));
  • 조회 대상을 여러건 지정할 때 multiselect를 사용하면 된다.
CriteriaBuilder cb = em.getCriteriabUILDER();
//JPQL : select m.username, m.age
cq.select(cb.array(m.get("username"), m.get("age")));
  • 여러 건 지정은 cb.array를 사용해도 된다.

DINTINCT

select, multiselect다음에 distinct(true)를 사용하면 된다.

JPQL : select distinct m.username, m.age from Member m

CriteriaQuery<Object[]> 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.get("age"))).distinct(true);

TypedQuery<Object[]> query = em.createQuery(cq);
List<Object[]> resultList = query.getResultList();

NEW.construct()

JPQL에서 select new 생성자() 구문을 Criteria에서는 cb.construct(클래스타입, ...)로 사용한다.

//JPQL: select new jpabook.domain.MemberDTO(m.username, m.age)
//from Member m

CriteriaQuery<MemberDTO> cq = cb.createQuery(MemberDTO.class);
Root<Member> m = cq.from(Member.class);

cq.select(cb.construct(MemberDTO.class, m.get("username"), m.get("age")));

TypedQuery<MemberDTO> query = em.createQuery(cq);
List<MemberDTO> resultList = query.getResultList();
  • JPQL 에서는 패키지명을 다 적어주지만 Criteria는 코드를 직접 다루기 때문에 패키지명 생략이 가능하다.

집합

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<Object[]> 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"));
 
 TypedQuery<Object[]> query = em.createQuery(cq);
 List<Object[]> resultList = query.getResultList();
  • cq.groupBy(m.get("team").get("name"))은 JPQL에서 group by m.team.name 과 같다.

HAVING

  • 위와 같은 조건에 나이 어린 사람이 10살을 초과하는 팀을 조회하는 조건을 추가해보자
cq.multiselect(m.get("team").get("name"), maxAge, minAge)
	.groupby(m.get("team").get("name"))
    .having(cb.gt(minAge, 10));
  • having(cb.ge(minAge, 10))은 JPQL에서 having min(m.age) > 10 과 같다.

정렬

cb.desc(...) 또는 cb.asc(...)로 생성할 수 있다.

cq.select(m
	.where(ageGt)
    .orderBy(cb.desc(m.get("age")));

JOIN

public enum JoinType {
	INNER,
    LEFT,
    RIGHT
    // JPA 구현체나 DB에 따라 지원하지 않을 수 있다.
}
  • JoinType 클래스
/* JPQL
	select m,t from Member m
    inner join m.team t
    where t.name = '팀A'
*/

Root<Member> m = cq.from(Member.class);
Join<Member, Team> t = m.join("team", JoinType.INNER);

cq.multiselect(m, t)
	.where(cb.equal(t.get("name"), "팀A"));
  • 쿼리 루트에서 바로 m.join("team") 메소드를 사용해서 회원과 팀을 조인했고
  • 조인한 team에 t라는 별칭을 줬다.
  • 조인 타입을 생략하면 내부 조인을 사용한다.
  • 페치 조인시 주의사항은 JPQL과 같다.

서브 쿼리

  • 평균 나이 이상의 회원을 구하는 서브쿼리 (메인과 서브가 상호 관련 x)
/* 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));

메인쿼리와 서브쿼리가 상호관련 있을 경우

/* 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);

//서브쿼리의 생성
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();
  • 핵심은 subQuery.correlate(m)이다.
  • correlate(...) 메서드를 사용하면 메인 쿼리의 별칭을 서브 쿼리에서 사용할 수 있다.

IN식

  • Criteria 빌더에서 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() 메서드를 사용한다.

/* JPQL
	select m.username,
    case when m.age>=60 then 600
    	 when m.age<=15 then 500
         else 1000
    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 
	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);

//정의
cq.select(m)
	.where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));
List<Member> resultList = em.createQuery(cq)
	.setParameter("usernameParam", "회원1")
    .getResultList();
  • cb.parameter(타입, 파라미터 이름) 메서드를 사용해서 파라미터를 정의했다.
  • setParameter("usernameParam", "회원1") 을 사용해서 해당 파라미터에 사용할 값을 바인딩했다.
  • Criteria에서는 파라미터를 정의하지 않고 직접 값을 입력해도 실제 SQL에서는 PreparedStatement에 파라미터 바인딩을 사용하고 있다.
//java
cq.select(m)
	.where(cb.equal(m.get("username"), "회원1"));
//sql
select * from Member m where m.name=?

네이티브 함수 호출

cb.function(...) 메서드를 사용하면 된다.

Root<Member> m = cq.from(Member.class);
Expression<Long> function = cb.function("SUM", Long.class, m.get("age"));
cq.select(function);
  • 전체 회원의 나이 합을 구하는 코드
  • SUM 대신에 원하는 네이티브 SQL 함수를 입력하면 된다.
  • 하이버네이트 구현체는 방언에 사용자 정의 SQL함수를 등록해야 호출할 수 있다.

0개의 댓글