43. 객체지향 쿼리 언어(10)

43. 객체지향 쿼리 언어(10)

3. Criteria

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

Criteria는 결국 JPQL의 생성을 돕는 클래스 모음이다. 따라서 내용 대부분이 JPQL과 중복되므로 사용법 위주로 알아보자.

1. Criteria 기초

Criteria API는 javax.persistence.criteria 패키지에 있다.

아래 예제를 통해 가장 단순한 Criteria 쿼리부터 살펴보자.

// JPQL : select m from Member m

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

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

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

cq.select(m);   // SELECT 절  4)

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

모든 회원 엔티티를 조회하는 단순한 JPQL을 Criteria로 작성해보자. 이해를 돕기 위해 Criteria의 결과로 생성된 JPQL을 첫 줄에 주석으로 남겨두었다.

1) Criteria 쿼리를 생성하려면 먼저 Criteria 빌더(CriteriaBuilder)를 얻어야 한다. Criteria 빌더는 EntityManager나 EntityMangerFactor에서 얻을 수 있다.

2) Criteria 쿼리 빌더에서 Criteria 쿼리(CriteriaQuery)를 생성한다. 이때 반환 타입을 지정할 수 있다.

3) FROM 절을 생성한다. 반환된 값 m은 Criteria에서 사용하는 특별한 별칭이다. m을 조회의 시작점이라는 의미로 쿼리 루트(Root)라 한다.

4) SELECT 절을 생성한다.

이렇게 Criteria 쿼리를 완성하고 나면 다음 순서는 JPQL과 같다. em.createQuery(cq)에 완성된 Criteria 쿼리를 넣어주기만 하면 된다.

이번에는 아래 예제에 검색 조건(where)과 정렬(order by)을 추가해보자.

//JPQL
//select m from Member m
//where m.username='회원1'
//order by m.age desc

CriteriaBuilder cb = em.getCriteriaBuilder();

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

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

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

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

// 쿼리 생성 3)
cq.select(m)
    .where(usernameEqual)    // WHERE 절 생성
    .orderBy(ageDesc);       // ORDER BY 절 생성

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

이전에 보았던 기본 쿼리에 검색 조건과 정렬 조건을 추가했다.

1) 검색 조건을 정의한 부분을 보면 m.get("username")으로 되어 있는데 m은 회원 엔티티의 별칭이다. 이것은 JPQL에서 m.username과 같은 표현이다. 그리고 cb.equal(A,B)는 이름 그대로 A = B라는 뜻이다. 따라서 cb.equal(m.get("username"), "회원1")는 JPQL에서 m.username = '회원1'과 같은 표현이다.

2) 정렬 조건을 정의하는 코드인 cb.desc(m.get("age"))는 JPQL의 m.age desc와 같은 표현이다.

3) 만들어둔 조건을 where, orderBy에 넣어서 원하는 쿼리를 생성한다.

Criteria는 검색 조건부터 정렬까지 Criteria 빌더(CriteriaBuilder)를 사용해서 코드를 완성한다.

쿼리 루트(Query Root)와 별칭을 알아보자.

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

Criteria는 코드로 JPQL을 완성하는 도구다. 따라서 경로 표현식도 있다.

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

cb.greaterThan(m.<Integer>get("age"), 10)을 보면 메소드 이름을 보고 A > B라는 것이 바로 이해가 될 것이다. 약간 의아한 부분은 m.<Integer>get("age")에서 제네릭으로 타입 정보를 준 코드다. m.get("age")에서는 "age"의 타입 정보를 알지 못한다. 따라서 제네릭으로 반환 타입 정보를 명시해야 한다(보통 String 같은 문자 타입은 지정하지 않아도 된다). 참고로 greaterThan() 대신에 gt()를 사용해도 된다.

이제 본격적으로 Criteria API를 살펴보자.

2. Criteria 쿼리 생성

Criteria를 사용하려면 아래 예제와 같은 CriteriaBuilder.createQuery() 메소드로 Criteria 쿼리(CriteriaQuery)를 생성하면 된다.

public interface CriteriaBuilder {

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

위의 예제를 보면 Criteria 쿼리를 생성할 때 파라미터로 쿼리 결과에 대한 반환 타입을 지정할 수 있다. 예를 들어 CriteriaQuery를 생성할 때 Member.class를 반환 타입으로 지정하면 em.createQuery(cq)에서 반환 타입으로 지정하지 않아도 된다.

CriteriaBuilder cb = em.getCriteriaBuilder();

// Member를 반환 타입으로 지정
CriteriaQuery<Member> cq = cb.createQuery(Member.class);
...

// 위에서 Member를 타입으로 지정했으므로 지정하지 않아도 Member 타입을 반환
List<Member> resultList = em.createQuery(cq).getResultList();

반환 타입을 지정할 수 없거나 반환 타입이 둘 이상이면 아래 예제와 같이 타입을 지정하지 않고 Object로 반환받으면 된다.

CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Object> cq = cb.createQuery();   // 조회값 반환 타입 : Object
...
List<Object> resultList = em.createQuery(cq).getResultList();

물론 반환 타입이 둘 이상이면 아래 예제와 같이 Object[]를 사용하는 것이 편리하다. 반환 타입이 둘 이상인 예제는 바로 뒤에 있는 multiselect에서 보자.

CriteriaBuilder cb = em.getCriteriaBuilder();

// 조회값 반환 타입 : Object[]
CriteriaQuery<Object[]> cq = cb.createQuery(Object[].class);
...
List<Object[]> resultList = em.createQuery(cq).getResultList();

반환 타입을 튜플로 받고 싶으면 아래 예제와 같이 튜플을 사용하면 된다. 튜플에 대해서는 조금 뒤에 알아보겠다.

CriteriaBuilder cb = em.getCriteriaBuilder();

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

3. 조회

이번에는 SELECT 절을 만드는 아래 예제에 정리한 select()에 대해서 알아보자.

public interface CriteriaQuery<T> extends AbstractQuery<T> {

    // 한 건 지정
    CriteriaQuery<T> select(Selection<? extends T> selection);
    
    // 여러 건 지정
    CriteriaQuery<T> multiselect(Selection<?>... selections);
    
    // 여러 건 지정
    CriteriaQuery<T> multiselect(List<Selection<?>> selectionList);
    ...
}

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

select에 조회 대상을 하나만 지정하려면 다음처럼 작성하면 된다.

cq.select(m)     // JPQL : select m

조회 대상을 여러 건 지정하려면 multiselect를 사용하면 된다.

// JPQL : select m.username, m.age
cq.multiselect(m.get("username"), m.get("age"));

여러 건 지정은 다음처럼 cb.array를 사용해도 된다.

CriteriaBuilder cb = em.getCriteriaBuilder();
// JPQL : select m.username, m.age
cq.select(cb.array(m.get("username"), m.get("age")));

DISTINCT

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

// JPQL : select distinct m.username, m.age
cq.multiselect(m.get("username"), m.get("age")).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(클래스 타입, ...)로 사용한다.

<Y> CompoundSelection<Y> construct(Class<Y> resultClass, Selection<?>... selections);

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에서는 select new jpabook.domain.MemberDTO()처럼 패키지명을 다 적어 주었다. 하지만 Criteria는 코드를 직접 다루므로 MemberDTO.class처럼 간략하게 사용할 수 있다.

튜플

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"),    // 튜플에서 사용할 튜플 별칭 1)
        m.get("age").alias("age")
);

TypedQuery<Tuple> query = em.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tuple tuple : resultList) {
    // 튜플 별칭으로 조회 2)
    String username = tuple.get("username", String.class);
    
    Integer age = tuple.get("age", Integer.class);
}

튜플을 사용하려면 cb.createTupleQuery() 또는 cb.createQuery(Tuple.class)로 Criteria를 생성한다.

1) 튜플은 튜플의 검색 키로 사용할 튜플 전용 별칭을 필수로 할당해야 한다. 별칭은 alias() 메소드를 사용해서 지정할 수 있다.
2) 선언해둔 튜플 별칭으로 데이터를 조회할 수 있다.

튜플은 이름 기반이므로 순서 기반의 Object[]보다 안전하다. 그리고 tuple.getElements() 같은 메소드를 사용해서 현재 튜플의 별칭과 자바 타입도 조회할 수 있다.

튜플에 별칭을 준다고 해서 실제 SQL에 별칭이 달리는 것은 아니다. 튜플은 Map과 비슷한 구조여서 별칭을 키로 사용한다.

튜플은 아래 예제와 같이 엔티티도 조회할 수 있다. 튜플을 사용할 때는 별칭을 필수로 주어야 하는 것에 주의하자.

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.createQuery(cq);
List<Tuple> resultList = query.getResultList();
for (Tule tuple : resultList) {
    Member member = tuple.get("m", Member.class);
    String username = tuple.get("username", String.class);
}

위의 예제에서 cq.multiselect(...) 대신에 cq.select(cb.tuple(...))를 사용했는데 둘은 같은 기능을 한다.

참고

  • 자바 ORM 표준 JPA 프로그래밍

좋은 웹페이지 즐겨찾기