Backend/JPA

[자바 ORM 표준 JPA 프로그래밍] 10장. Criteria 쿼리

anxi 2024. 6. 21. 00:18
회사에서 ElasticSearch 조회할 때 CriteriaQuery 만들어봤는데 JPA에도 Criteria 쿼리가 있다니.. 신기방기

 

Criteria

JPQL을 자바 코드로 작성할 수 있게 도와주는 빌더 클래스 API

코드로 JPQL을 작성함으로써 컴파일 단계에서 문법 오류를 잡을 수 있고 동적 쿼리를 안전하게 생성 가능

코드가 복잡하고 장황해서 직관적으로 이해하기 힘든 단점

 

기초

//JPQL: select m from Member m

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

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

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

TypedQuery<Member> query = em.createQuery(cq);
List<Member> members = query.getResultList();

 

  1. Criteria 쿼리를 작성하기 위해 빌더를 생성한다. (EntityManager를 통해 얻음)
  2. 빌더에서 Criteria 쿼리를 생성한다. 이때 반환 타입 지정한다.
  3. From 절을 생성한다. 위 코드의 m은 Criteria에서 사용하는 별칭으로, m을 조회의 시작점이라 해서 "Root"라고 한다.
  4. select 절을 생성한다.

상기 코드에 검색 조건과 정렬 조건 또한 추가할 수 있다.

// 검색 조건
Predicate usernameEqual = cb.equal(m.get("username"), "검색값");

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

// 쿼리 생성
cq.select(m)
  .where(usernameEqual)
  .orderBy(ageDesc);

 

  • 쿼리 루트는 조회의 시작점이다. (Root<> ~ = cq.from(~));
  • Criteria에서 사용되는 특별한 별칭이다. (상기 Member m)
  • 별칭은 엔티티에만 부여할 수 있다.

생성

public interface CriteriaBuilder {
    CriteriaQuery<Object> createQuery (); //조회값 반환 타입: Object

    //조회값반환타입:엔티티,임베디드타입,기타
    <T> CriteriaQuery<T> createQuery(Class<T> resultclass);
    ...
}

 

createQuery 실행 시 파라미터로 쿼리 결과에 대한 반환 타입을 지정할 수 있어

만약 위에서 createQuery(Member.class)를 통해 쿼리를 생성했다면, em.createQuery(cq)에선 반환 타입을 지정하지 않아도 된다.

CriteriaQuery<Member> cq = cb.createQuery(Member.class);

List<Member> resultList = em.createQuery(cq).getResultList();

 

혹은 파라미터 없이 createQuery()를 사용하여 Object를 반환하기도 한다.

반환 타입이 둘 이상일 경우 Object[]를 사용하는 것도 편리하다.

CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
List<Object[]> resultList = em.createQuery(cq).getResultList();

 

조회

조회는 select() 메서드를 통해 할 수 있다.

 

조회 대상을 한 건 혹은 여러 건 지정할 수 있다.

  • 조회 대상 : 1건
    • cq.select(m);
  • 조회 대상 : 여러 건
    • cq.multiselect(m.get("username"), m.get("age")); 
    • 여러건 지정은 cb.array를 사용할 수도 있다.
      cq.select(cb.array(m.get("username"), m.get("age"))); 

DISTINCT

distince는 select, multiselect 다음에 distinct(true)를 사용한다.

cq.multiselect(m.get("username"), m.get("age")).distinct(true);

 

NEW, construct()

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

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

 

튜플

Criteria는 Map과 비슷한 튜플이라는 특별한 반환 객체를 제공한다.

CriteriaQuery<Tuple> cq = cb.createTupleQuery();
// CriteriaQuery<Tuple> cq = cb.createQuery(Tuple.class);
// createTupleQuery()를 사용하거나 createQuery(Tuple.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);
}

 

Map에서 Key에 해당하는 별칭을 설정해주고, Value를 별칭을 통해 얻을 수 있다.

따라서 별칭은 필수로 할당해야 한다.

cq.select(cb.tuple(...));

 

또한 cq.multiselect() 대신 위와 같이 사용해도 동일한 결과를 반환한다.


집합

  • GROUP BY : cq.groupBy(m.get("team").get("name")) 와 같이 사용한다.
  • HAVING : cq.having(cb.gt(minAge, 10)) 와 같이 사용한다. 여기서 gt는 min(m.age) > 10과 같다. (greater than)

정렬

  • 정렬은 CriterBuilder를 사용한다. 
  • cb.desc(...), cb.asc(...) 를 사용한다.
cq.select(m)
  .where(ageGt)
  .orderBy(cb.desc(m.get("age")));

조인

조인은 join() 메소드와 JoinType ENUM 클래스를 사용한다.

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

 

  • 조인 타입을 생략하면 내부 조인을 사용한다.
  • 외부 조인은 JoinType.LEFT로 설정한다. (RIGHT는 하이버네이트나 구현체에 따라 없을 수 있다.)
  • FETCH JOIN은 fetch(조인 대상, JoinType)를 사용한다.
    • m.fetch("team", JoinType.LEFT);
      cq.select(m);

서브 쿼리

/* JPQL:
select m from Member m
  where m.age >=
    (select AVG(m2.age) from Member m2)
*/

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

 

단순한 쿼리는 mainQuery.subQuery(...)로 생성한다.

 

만약 서브 쿼리에서 메인 쿼리의 정보를 사용한다면 메인 쿼리에서 사용한 별칭을 사용한다.

/* JPQL
select m from Member m
  where exists
    (select t from m.team t where t.name = '팀A')
*/

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

//서브 쿼리 생성
Subquery<Team> subQuery = mainQuery.subquery(Team.class);
Root<Member> subM = subQuery.correlate(m); // mainQuery의 별칭을 가져온다.
Join<Member, Team> t = subM.join("team")
...

 

IN, CASE

  • IN : CriteriaBuilder의 in(...) 메소드 사용
    • where(cb.in(...))
          .value(...)
          .value(...)
          ...
  • CASE : CriteriaBuilder의 selectCase()와 when(<조건 절>, <then 절>).otherwise(else 절)을 사용한다.

파라미터 정의

/* JPQL
select m from Member m
  where m.username = :usernameParam
*/

cq.select(m)
  .where(cb.equal(m.get("username"), cb.parameter(String.class, "usernameParam")));

List<Member> resultList = em.createQuery(cq)
    .setParameter("usernameParam", "멤버") // 파라미터 바인딩
    .getResultList();
  • cb.parameter(파라미터 타입, 파라미터 이름) 으로 파라미터 정의
  • setParameter(파라미터 이름, 값) 으로 파라미터 사용할 값 바인딩