본문 바로가기

3.구현/Java or Kotlin

[kotlin] JPA 8: Spring Boot에서 엔티티 매니저와 Criteria 쿼리

들어가기

JPQL 작성을 도와주는 API로 Criteria Query와 QueryDSL이 있다. Criteria Query는 표준이지만 QueryDSL은 표준은 아니다. 사용에 있어서는 QueryDSL이 더 좋기 때문에 알아두면 좋다. 여기는 JPA 표준인 Criteria Query에 대해서 다룰려고한다. 쿼리에 대한 자세한 설명이 이전 글을 참고하시기 바란다. 여기서는 Criteria Query 작성에 집중했다.

작성자: http://ospace.tistory.com/ (ospace114@empal.com)

Entity Manager 사용

Criteria Query를 사용하기 위해 기존 리포지토리를 확장해서 엔티티 매니저(EntityManager)를 가져와야 한다. 엔티티 매니저는 엔티티 객체를 영속 컨텍스트(persistence context)라는 저장소에 저장하고 관리한다. 그리고 데이터베이스에 반영한다.

Fig 01.Entity Manager 계층도

엔티티 매니저(EntityManager)는 엔티티 매니저 팩토리(EntityManagerFactory)에 의해서 생성되고 관리된다. 엔티티 매니저 팩토리는 PersistenceUnit에 의해 설정되고 싱글톤 형태로 애플리케이션에서 전체에 공유된다. 생성된 엔티티 매니저는 엔티티의 연속성을 관리하고 데이터베이스 CRUD 작업을 처리한다. 이를 위해 엔티티 매니저가 데이터베이스 커텍션을 관리한다. 그렇기에 사용이 끝난 엔티티 매니저는 데이터베이스 커넥션을 종료해줘야 한다.

val emf: EntityManagerFactory = Persistence.createEntityManagerFactory("persistenceUnitName")
val em: EntityManger = emf.createEntityManager()
// ...
em.close() // 엔티티 매니저 사용 종료시
emf.close() // 애플리케이션 종료시

엔티티 매니저는 애플리케이션 관리(application-managed)와 컨테이너 관리(container-managed) 종류로 구분할 수 있다. 애플리케이션 관리는 애플리케이션에서 직접 엔티티 매니저를 관리하고 컨테이너 관리는 JEE 컨테이너(JBoss, WebLogic 등)가 엔티티 매니저를 관리한다.

엔티티 객체는 영속 객체로 엔티티 매니저에 의해서 관리된다.

  • 새로운 상태(transient): 엔티티 객체가 새로 생성된 상태, 아직 연속 컨텍스트에 저장안된 상태
  • 관리 상태(managed): 영속 컨텍스트에 저장된 상태
  • 분리된 상태(detached): 엔티티 매니저가 종료되거나 detach로 영속 컨텍스트에서 분리된 상태
  • 삭제된 상태(removed): 영속 켄텍스트에서 삭제된 상태

Fig 02. 영속 상태

이런 엔티티 객체는 엔티티 매니저의 영속 컨텍스트 저장소에 저장되며 관리 상태가 된다. 이때 엔티티는 식별자(@Id로 지정)가 있어야 하고 없으면 에러가 발생한다. 또한 영속 컨텍스트는 캐시 역활도 하며 트랜잭션을 지원한다.

엔티티 매니저에 의해서 상태 관리를 살펴보자.

val em = emf.createEntityManager()
val member = Member() // 새로운 상태
em.persist(member)    // 관리 상태
em.detach(member)     // 분리된 상태
em.merge(member)      // 관리 상태
em.remove(member)     // 삭제된 상태

persist()에 의해서 엔티티가 영속 컨텍스트에 저장되고 detach()는 해당 엔티티를 영속 컨텍스트에서 분리된다. 또한 merge()은 분리된 엔티티를 다시 영속 컨텍스트에 저장한다. 그리고 remove()은 에티티를 영속 컨텍스트에서 삭제한다. 이외에도 다양한 영속 관리를 위한 메소드가 있다.

영속 컨텍스트는 데이터베이스와 동기화는 트랜잭션으로 커밋(commit)과 롤백(rollback)으로 처리하거나 flush등으로 데이터베이스에 반영된다.

val em = emf.createEntityManager()
val member = em.find(Member::class.java)
//...
em.flush()
em.close()

트랜잭션 커밋도 내부적으로는 flush를 호출한다.

val em = emf.createEntityManager()
val tx = em.getTransaction()
try {
    tx.begin()
    //...
    tx.commit()   // 반영
} catch(ex: Exception) {
    tx.rollback() // 복원
    throw ex
} finally {
  em.close()
}

트랜잭션을 잘 활용하면 내부에 트랜잭션 쓰기 지연(transactional write-behind) 저장소에 변경을 위한 쿼리들을 한번에 반영해주기 때문에 성능상 이점이 있다.

참고로 JPA에서 엔티티 업데이트 전략은 모든 속성을 사용한다. 즉, 수정 쿼리에 모든 속성이 포함되어 실행한다. 이로 인해 전송할 데이터량은 증가하지만, 쿼리가 단순해지고 데이터베이스에서는 같은 쿼리로 인해 성능은 증가한다.

Spring Boot의 Spring Data에서는 자동으로 관리되기 때문에 특별한 경우가 아니라면 엔티티 매니저를 신경쓸 필요는 없다. Spring Boot에서 엔티티 매니저를 통해 간단하게 엔티티를 처리하는 방식을 살펴보자.

interface IMemberRepository {
    fun getMembers(): List<Member>
}

interface MemberRepository : Repository<Member, Long>, IMemberRepository {
}

MemberRepository 인터페이스는 Repository 인터페이스를 상속하고 Criteria Query를 위한 IMemberRepository 인터페이스를 상속했다. 여기까지 보면 기존 쿼리 메소드와 동일한 형태이다. 이제 IMemberRepository 인터페이스의 구현체인 MemberRepositoryImpl 클래스에서 getMembers()를 Criteria Query를 사용해 구현해보자.

@Repository
open class MemberRepositoryImpl(
    @PersistenceContext
    private val em: EntityManager,
) : IMemberRepository {
    override fun getMembers(): List<Member> {
        val cb = em.criteriaBuilder
        val cq = cb.createQuery(Member::class.java)
        val m = cq.from(Member::class.java)
        cq.select(m)
        val query = em.createQuery(cq)
        return query.resultList
    }
}

MemberRepositoryImpl 클래스에서 EntityManager 인스탄스를 주입받고 이를 통해 Criteria Query를 사용해 getMembers()를 정의했다. 엔티티 매니저 인스탄스를 주입받기 위해서는 @PersistenceContext 어노테이션을 사용한다.

다른 빈 객체에서 MemberRepository 디펜던시로 선언하면 스프링에서는 구현체를 검색하고 이를 통해 인터페이스에 있는 구현된 메소드를 호출한다. 이 방법으로 JPA에서 제공되는 메소드와 새로 추가된 메소드를 제공할 수 있게 된다.

앞으로 EntityManager 객체인 em 속성이 있다고 가정해서 진행하겠다.

Criteria Query

Criteria Query는 JPQL 생성을 지원하는 빌더 클래스 API이다. 이전에는 @Query 어노테이션이 대신 역활을 진행했다. Criteria Query는 직접 실행하고 좀더 유연하고 정교하게 처리할 수 있게 한다. 물론 그만큼 해야할 작업이 많아진다.

아래는 앞으로 나오는 인터페이스들의 상속 계층도이다. 메소드를 볼때 참고하면 된다.

Fig 03. Criteria Query 계층도

Select 문

앞의 Select 예제코드를 하나씩 살펴보자. 먼저 간단한 Member 엔티티 조회 JPQL 쿼리이다.

SELECT m FROM Member AS m

물론 엔티티 매니저의 createQuery()에 아래와 같이 JPQL 쿼리 문자열을 넘겨서 실행할 수 있다.

override fun getMembers(): List<Member> {
    val query = em.createQuery("SELECT m FROM Member m", Member::class.java)
    return query.resultList
}

위의 JPQL 쿼리에서 파라미터 바인딩이 필요하다면아래 처럼 하면 된다.

// 이름 파라미터
query.setParameter("name", name)
// 위치 파라미터
query.setParameter(1, name)

이 방법은 이렇게 직접 JPQL 쿼리를 코드에 입력하는 것 보다 @Query 사용을 추천한다.

여기서는 Criteria Query을 작성하는 법을 살펴보자.

override fun getMembers(): List<Member> {
    val cb = em.criteriaBuilder
    val cq = cb.createQuery(Member::class.java)
    val m = cq.from(Member::class.java)
    cq.select(m)
    val query = em.createQuery(cq)
    return query.resultList
}

엔티티 매니저에서 criteriaBuilder를 통해 Criteria 빌더를 획득한다. Criteria 빌더의 createQuery()을 통해 리턴 타입이 Member인 Criteria 쿼리 생성한다.

public interface CriteriaBuilder {
    <T> CriteriaQuery<T> createQuery(Class<T> resultClass);
    CriteriaQuery<Object> createQuery();
}

createQuery()에 의해서 CriteriaQuery 객체를 리턴한다. 인자로는 리턴되는 타입의 클래스 정보를 받을 수 있고 또는 리턴 타입을 지정하지 않으면 기본 타입이 Object로 생성된다. 리턴 타입을 지정하지 않을 경우 CriteriaQuery에서 결과 타입이 Object으로 가져오기 때문에 결과를 가져올때 캐스팅해줘야 한다.

Criteria 쿼리에서 from()에 의해 가져올 엔티티를 지정하고 쿼리 루트 객체를 얻는다.

public interface AbstractQuery<T> extends CommonAbstractCriteria {
    <X> Root<X> from(Class<X> entityClass);
    <X> Root<X> from(EntityType<X> entity);
}

from()은 AbstractQuery 인터페이스에 선언되어 있지만, CriteriaQuery 인터페이스가 AbstractQuery 인터페이스를 상속하고 있어서 CriteriaQuery를 통해서 호출할 수 있다.

from()에 의해 가져올 대상이 되는 엔티티 클래스 타입 또는 EntityType으로 지정하면 쿼리 루트에 추가되고 Root 객체를 리턴한다.

이를 select()에 넘겨서 기본 조회 쿼리를 완성한다. 가장 기본적인 조회 쿼리인 “SELECT … FROM …”형태가 되었다.

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()에 의해서 조회할 대상을 지정한다. 이때 조회할 대상은 1개인 경우이다. 만약 여러 개를 조회할 경우 multiselect()을 사용한다. 여러 개를 지정할 때 가변 인자로 사용하거나 List 형태로 넘겨줄 수 있다.

그리고 엔티티 매니저에 createQuery()에 생성한 Criteria 쿼리를 전달해서 쿼리 객체를 생성한다.

public interface EntityManager extends AutoCloseable {
    <T> TypedQuery<T> createQuery(CriteriaQuery<T> criteriaQuery);
}

createQuery()에 CriteriaQuery 객체를 넘겨서 호출하면 Criteria 쿼리를 실행하기 위한 TypedQuery 객체를 리턴한다. resultList를 액세스하면 getResultList()가 호출된다.

결과를 얻기 위해 resultList를 조회하면 해당 쿼리가 실행되고 데이터 목록을 리턴한다. 만약 가져올 데이터가 한개이면 singleResult을 사용한다. 이 경우 데이터가 없을 경우 NoResultException이 발생하고 데이터가 2개 이상이면 NonUniqueResultException예외가 발생한다.

public interface TypedQuery<X> extends Query {
    List<X> getResultList();
    X getSingleResult();
}

간단하게 Criteria 쿼리 생성하고 실행해서 결과까지 얻는 과정을 설펴보았다. Criteria 쿼리 장점은 직접 코딩해서 쿼리를 작성하기 때문에 컴파일 타임에 쿼리 오류를 확인할 수 있고, IDE에 의해서 자동완성 기능도 사용할 수 있다. 또한 동적으로 복잡한 쿼리를 재사용할 수 있다. 앞의 JPQL 쿼리는 런타임에 쿼리 오류를 확인할 수 있고, 고정된 쿼리고 가변적으로 사용하기 힘들다.

비교 조건을 추가해보자.

SELECT m FROM Member AS m WHERE m.name='foo'

이번에는 이름을 받아서 멤버를 찾는 예제이다.

override fun getMemberByNname(name:String): Member {
    //...
    val namePredicate = cq.equal(m.get("name"), name) // 비교 조건
    cq.where(namePredicate) // 조건 추가
    val query = em.createQuery(cq)
    return query.singleResult
}

getMemberByNname()에서 where()을 추가한다. m.get()에 의해 Member 엔티티의 속성 값을 획득할 수 있다. 그리고 equal()에 의해 인자 name과 동일한지 비교 한다. 이 비교 조건을 CriteriaQuery의 where()으로 입력한다.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
    CriteriaQuery<T> where(Expression<Boolean> restriction);
    CriteriaQuery<T> where(Predicate... restrictions);
}

distinct()을 사용하는 방법도 알아보자.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
    CriteriaQuery<T> distinct(boolean distinct);
}

distince()도 CriteriaQuery에 포함되었다. 사용하는 예이다.

cq.select(m).distinct(true)

GROUP BY, HAVING

집합 쿼리에 대해서 살펴보자. 다음과 같은 JPQL 쿼리를 보자.

SELECT 
  m.subscription.grade AS grade,
  COUNT(m.name) AS cnt,
  MAX(m.likes) AS max_likes,
  AVG(m.likes) AS avg_likes,
  MIN(m.likes) AS min_likes
FROM Member m
GROUP BY m.subscription.grade
HAVING MIN(m.points) > 0

이를 Criteria Query로 작성하면 다음과 같다.

override fun getMembersByGrade(): List<Tuple> {
    val cb = em.criteriaBuilder
    val cq = cb.createTupleQuery()
    val m = cq.from(Member::class.java)
    val maxLikes = cb.max(m.get<Int>("likes")).alias("max_likes")
    val avgLikes = cb.avg(m.get<Int>("likes")).alias("avg_likes")
    val minLikes = cb.min(m.get<Int>("likes")).alias("min_likes")
    val grade = m.get<Subscription>("subscription").get<Grade>("grade")

    cq.multiselect(grade.alias("grade"), cb.count(m).alias("cnt"), maxLikes, avgLikes, minLikes)
        .groupBy(grade)
        .having(cb.gt(m.get<Int>("likes"), 0))
    val query = em.createQuery(cq)
    return query.resultList
}

앞의 기본적인 부분은 Select 문과 동일하다. createTupleQuery()로 리턴 타입을 튜플로 지정했다. 튜플은 특별히 저장할 객체가 없을 경우 사용할 수 있다. 그리고 multiselect()에서 집합함수가 사용되었다. alias()을 이용해 별칭을 부여했고, 튜플에 저장할 때 사용된다. 튜플 사용시 별칭이 없다면 에러가 발생하기에 주의가 필요하다.

그리고, 집합 쿼리인 GROUP BY는 groupBy()을 사용한다.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
    CriteriaQuery<T> groupBy(Expression<?>... grouping);
}

groupBy()에 의해 멤버의 구독 정보에서 grade을 가지고 분류한다. 그리고 HAVING에 의한 집합 조건에 의해서 필터링된다.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
    CriteriaQuery<T> having(Expression<Boolean> restriction);
    CriteriaQuery<T> having(Predicate... restrictions);
}

비교 함수를 사용해서 멤버의 points가 0보다 큰 값만 추출한다. 그리고 결과는 Map 객체 목록으로 리턴된다.

여기서 튜플은 바로 JSON으로 변환을 하여 리턴할 수 없다. 직접 JSON으로 변환하거나 또는 Map 객체로 변환해서 사용해야 한다.

val ret = memberRepo.getMembersByGrade()
return ret.map { each ->
    val res = HashMap<String, String>()
    each.elements.forEach { res[it.alias] = each.get(it.alias).toString() }
    res
}

위처럼 목록에서 튜플 객체 가져와서 Map 객체로 변환하고 있다.

New

NEW을 위해 construct() 사용해서 결과 저장될 객체를 인스탄스화해서 사용할 수 있다.

public interface CriteriaBuilder {
    <Y> CompoundSelection<Y> construct(Class<Y> resultClass, Selection<?>... selections);
}

앞에 getMembersByGrade()에 적용한 예이다.

fun getMembersByGrade(): List<MemberGradeStatVo> {
    val cb = em.criteriaBuilder
    val cq = cb.createQuery(MemberGradeStatVo::class.java)
    val m = cq.from(Member::class.java)
    val maxLikes = cb.max(m.get<Int>("likes"))
    val avgLikes = cb.avg(m.get<Int>("likes"))
    val minLikes = cb.min(m.get<Int>("likes"))
    val grade = m.get<Subscription>("subscription").get<Grade>("grade")

    cq.select(
        cb.construct(MemberGradeStatVo::class.java, grade, cb.count(m), maxLikes, avgLikes, minLikes)
    )
        .groupBy(grade)
        .having(cb.gt(m.get<Int>("likes"), 0))
    val query = em.createQuery(cq)
    return query.resultList
}

cb.construct()에서 저장할 MemberGradeStatVo 클래스 정보와 초기화할 값들을 전달한다. 그리고 단일 객체로 리턴하기 때문에 cq.select()를 사용하고 있다. 굳이 alias()을 사용할 필요가 없다.

ORDER BY

ORDER BY에 의한 정렬 조건은 orderBy()로 추가할 수 있다.

public interface CriteriaQuery<T> extends AbstractQuery<T> {
    CriteriaQuery<T> orderBy(Order... o);
    CriteriaQuery<T> orderBy(List<Order> o);
}

간단한 사용 예이다.

cq.select(m)
  .orderBy(cb.desc(m.get<String>("name")))

오른차순은 asc(), 내림차순은 desc()를 사용한다.

JOIN

조인은 Join()과 JoinType 열거형을 사용한다.

public enum JoinType {
    INNER, // 내부 조인
    LEFT,  // 왼쪽 외부 조인
    RIGHT  // 오른족 외부 조인
}

public interface From<Z, X> extends Path<X>, FetchParent<Z, X> {
    <X, Y> Join<X, Y> join(String attributeName, JoinType jt);
}

타입 파라미터 X, Y가 있다. X는 대상 엔티티이고 Y는 조인될 엔티티를 의미한다. attributeName에 대상 엔티티에서 조인할 속성(연관 관계가 있는)을 지정한다. 그리고 jt에 조인할 형태를 지정한다.

사용 예를 보자. 다음 JPQL 쿼리를 작성해보자.

SELECT m FROM Member m JOIN m.subscription s WHERE s.grade = 'FREE'

이를 Criteria Query로 작성했다.

override fun getFreeMembers(): List<Member> {
    val cb = em.criteriaBuilder
    val cq = cb.createQuery(Member::class.java)
    val m = cq.from(Member::class.java)
    val s = m.join<Member, Subscription>("subscription", JoinType.INNER)
    cq.select(m).where(cb.equal(s.get<Grade>("grade"), Grade.FREE))
    val query = em.createQuery(cq)
    return query.resultList
}

만약 join()에서 JoinType을 지정하지 않으면 기본은 JoinType.INNER이다. 루트 쿼리에서 지정된 멤버 엔티티에 대해서 조인한다. 대상 엔티티가 Member가 되고 subscription 속성에 해당하는 Subscription 엔티티와 내부(INNER) 조인한다. WHERE 조건에 Subscription의 grade가 FREE인 경우를 추가했다.

FETCH JOIN은 앞에 join() 대신 fetch()을 사용해서 조인한다.

public interface FetchParent<Z, X> {
    <X, Y> Fetch<X, Y> fetch(String attributeName, JoinType jt);
}

FetchParent는 From에서 상속하기에 쿼리 루트에서 사용할 수 있다. 타입 파라미터 X는 대상 엔티티이고 Y는 조인될 엔티티이다. attributeName에 대상 엔티티에서 페치 조인할 속성(연관 관계가 있는)을 지정하고 jt에 조인 형태를 지정한다.

JPQL 쿼리 사용하는 예이다.

SELECT m FROM Member m JOIN FETCH m.subscription

이를 Criteria Query로 작성해 보자.

    override fun getMembers(): List<Member> {
        val cb = em.criteriaBuilder
        val cq = cb.createQuery(Member::class.java)
        val m = cq.from(Member::class.java)
        val s = m.fetch<Member, Subscription>("subscription", JoinType.LEFT)
        cq.select(m)
        val query = em.createQuery(cq)
        return query.resultList
    }

join()을 fetch()로만 변경했을 뿐이다. 조인 타입은 LEFT로 지정했다.

UPDATE와 DELETE 문

UPDATE와 DELETE는 기존 SELECT와는 조금 다르다. SELECT는 결과를 가져오지만 UPDATE와 DELETE는 성공 또는 실패만 있을 뿐이다. 이를 위해 createQuery() 대신 createCriteriaUpdate() 또는 createCriteriaDelete()을 사용해야한다.

public interface CriteriaBuilder {
    <T> CriteriaUpdate<T> createCriteriaUpdate(Class<T> targetEntity);
    <T> CriteriaDelete<T> createCriteriaDelete(Class<T> targetEntity);
}

먼저 UPDATE을 위한 JPQL 쿼리부터보자.

UPDATE Member m SET m.name='bar' WHERE m.id = 1

이를 Criteria Query작성하면 아래와 같다.

fun updateMemberName(id: Long, name:String) {
    val cb = em.criteriaBuilder
    val cq = cb.createCriteriaUpdate(Member::class.java)
    val m = cq.from(Member::class.java)
    cq.set("name", name)
    cq.where(cb.equal(m.get<Long>("id"), id))
    val query = em.createQuery(cq)
    query.executeUpdate()
}

DELETE을 위한 경우도 비슷하다.

DELETE FROM Member m WHERE m.id = 1

Criteria Query로 작성해보자.

fun deleteMember(name:String) {
    val cb = em.criteriaBuilder
    val cq = cb.createCriteriaDelete(Member::class.java)
    val m = cq.from(Member::class.java)
    cq.where(cb.equal(m.get<String>("name"), name))
    val query = em.createQuery(cq)
    query.executeUpdate()
}

삭제인 경우는 WHERE 조건만 지정하면 된다.

WHERE 문

앞에서 사용한 equal()외에도 greaterThan, greaterThanOrEqual, lessThan, lessThanOrEqual, or, and, not 등이 있다. 줄여서 gt, ge, lt, le으로도 사용할 수 있다.

public interface CriteriaBuilder {
    Predicate equal(Expression<?> x, Expression<?> y);
    Predicate equal(Expression<?> x, Object y);
    Predicate notEqual(Expression<?> x, Expression<?> y);
    Predicate notEqual(Expression<?> x, Object y);
    <Y extends Comparable<? super Y>> Predicate greaterThan(Expression<? extends Y> x, Expression<? extends Y> y);
    <Y extends Comparable<? super Y>> Predicate greaterThan(Expression<? extends Y> x, Y y);
    <Y extends Comparable<? super Y>> Predicate greaterThanOrEqualTo(Expression<? extends Y> x, Expression<? extends Y> y);
    <Y extends Comparable<? super Y>> Predicate greaterThanOrEqualTo(Expression<? extends Y> x, Y y);
    <Y extends Comparable<? super Y>> Predicate lessThan(Expression<? extends Y> x, Expression<? extends Y> y);
    <Y extends Comparable<? super Y>> Predicate lessThan(Expression<? extends Y> x, Y y);
    <Y extends Comparable<? super Y>> Predicate lessThanOrEqualTo(Expression<? extends Y> x, Expression<? extends Y> y);
    <Y extends Comparable<? super Y>> Predicate lessThanOrEqualTo(Expression<? extends Y> x, Y y);    
    Predicate gt(Expression<? extends Number> x, Expression<? extends Number> y);
    Predicate gt(Expression<? extends Number> x, Number y);
    Predicate ge(Expression<? extends Number> x, Expression<? extends Number> y);
    Predicate ge(Expression<? extends Number> x, Number y);
    Predicate lt(Expression<? extends Number> x, Expression<? extends Number> y);
    Predicate lt(Expression<? extends Number> x, Number y);
    Predicate le(Expression<? extends Number> x, Expression<? extends Number> y);
    Predicate le(Expression<? extends Number> x, Number y);
}

Expression에는 Subquery, ParameterExpression, Path, Case, Coalesce, In, SimpleCase가 올 수 있다. 물론 Predicate는 현재 비교 함수의 결과가 입력될 수 있다. 다양한 조건을 조합할 수 있다.

또한 AND, OR, NOT에 대한 논리연산자도 가능하다.

public interface CriteriaBuilder {
    Predicate and(Expression<Boolean> x, Expression<Boolean> y);
    Predicate and(Predicate... restrictions);
    Predicate or(Expression<Boolean> x, Expression<Boolean> y);
    Predicate or(Predicate... restrictions);
    Predicate not(Expression<Boolean> restriction);
}

Expression의 결과가 Boolean형과 Predicate을 입력 받을 수 있다. 그리고 결과도 Predicate가 된다.

이외에 다양한 비교 표현식이 있다.

public interface CriteriaBuilder {
    <Y extends Comparable<? super Y>> Predicate between(Expression<? extends Y> v, Expression<? extends Y> x, Expression<? extends Y> y);
    <Y extends Comparable<? super Y>> Predicate between(Expression<? extends Y> v, Y x, Y y);
    <T> In<T> in(Expression<? extends T> expression);
    Predicate like(Expression<String> x, Expression<String> pattern);
    Predicate like(Expression<String> x, String pattern);
    Predicate like(Expression<String> x, Expression<String> pattern, Expression<Character> escapeChar);
    Predicate like(Expression<String> x, Expression<String> pattern, char escapeChar);
    Predicate like(Expression<String> x, String pattern, Expression<Character> escapeChar);
    Predicate like(Expression<String> x, String pattern, char escapeChar);
    Predicate isNull(Expression<?> x);
    <C extends Collection<?>> Predicate isEmpty(Expression<C> collection);
    <E, C extends Collection<E>> Predicate isMember(Expression<E> elem, Expression<C> collection);
    <E, C extends Collection<E>> Predicate isMember(E elem, Expression<C> collection);
}

like은 notLike도 사용가능하다. 그리고 isNull, isEmpty, isMember도 isNotNull, isNotEmpty, isNotMember도 사용가능하다. 단지 결과가 반대가 될 뿐이다.

서브 쿼리

서브쿼리는 subquery()을 사용한다. CommonAbstractCriteria에 선언되었지만 CriteriaQuery에서 상속되어있다.

public interface CommonAbstractCriteria {
    <U> Subquery<U> subquery(Class<U> type);
}

JPQL 쿼리를 작성해보자.

SELECT m FROM Member m WHERE m.likes > (SELECT AVG(m1.likes) FROM Member m1)

Criteria Query 사용 예를 보자.

override fun getMembersOverAvg: List<Member> {
    val cb = em.criteriaBuilder
    val cq = cb.createQuery(Member::class.java)
    val m = cq.from(Member::class.java)
    val sq = cq.subquery(Double::class.java)
    val m2 = sq.from(Member::class.java)
    sq.select(cb.avg(m2.get<Int>("likes")))
    cq.select(m).where(cb.gt(m.get<Int>("likes"), sq))
    val query = em.createQuery(cq)
    return query.resultList
}

subquery()은 createQuery()와 비슷한 형태로 사용하면 된다. 똑같이 from()으로 쿼리 루트를 획득하고 select()에 의해서 조회한다. 그리고 where()조건에서 사용한다.

서브쿼리에서 사용할 수 있는 표현식으로 exists, all, any, some 등이 있다.

public interface CriteriaBuilder {
    Predicate exists(Subquery<?> subquery);
    <Y> Expression<Y> all(Subquery<Y> subquery);
    <Y> Expression<Y> some(Subquery<Y> subquery);
    <Y> Expression<Y> any(Subquery<Y> subquery);
}

이외에 논리 연산자나 비교 표현식도 같이 조건으로 사용할 수 있다.

표현식

앞에 나왔던 표현식 중이 일부를 다루었다.

IN

IN 표현식은 in()을 사용한다.

public interface CriteriaBuilder {
    public static interface In<T> extends Predicate {
         Expression<T> getExpression();
           In<T> value(T value);
         In<T> value(Expression<? extends T> value);
     }
    <T> In<T> in(Expression<? extends T> expression);
}

In 리턴 객체로 필요가 추가 값들을 설정한다. 사용예를 보자.

val inName = cb.`in`(m.get<String>("name"))
    .value("foo")
    .value("bar")
cq.select(m).where(inName)

in()이 Kotlin 키워드로 충돌이 발생하기 때문에 in을 backticks(`)로 감싸면 키워드로 해석하지 않는다.

CASE, WHEN

CASE 표현식은 selectCase()을 사용하고 Case 리턴 객체를 통해 when()과 otherwise() 조건을 설정할 수 있다.

public interface CriteriaBuilder {
    public static interface Case<R> extends Expression<R> {
        Case<R> when(Expression<Boolean> condition, R result);
        Case<R> when(Expression<Boolean> condition, Expression<? extends R> result);
        Expression<R> otherwise(R result);
        Expression<R> otherwise(Expression<? extends R> result);
    }
    <R> Case<R> selectCase();
}

사용 예이다.

cb.selectCase<String>()
    .`when`(cb.ge(m.get<Int>("likes"), 100), "VeryPopular")
    .`when`(cb.ge(m.get<Int>("likes"), 10), "Popular")
    .otherwise("Newbie")

when()이 Kotlin 키워드로 충돌이 발생하기 때문에 when을 backticks(`)로 감싸면 키워드로 해석하지 않는다.

함수

다양한 함수를 지원한다.

집합함수

public interface CriteriaBuilder {
    Expression<Long> count(Expression<?> x);
    <N extends Number> Expression<Double> avg(Expression<N> x);
    <N extends Number> Expression<N> sum(Expression<N> x);
    <N extends Number> Expression<N> max(Expression<N> x);
    <N extends Number> Expression<N> min(Expression<N> x);
       <C extends Collection<?>> Expression<Integer> size(Expression<C> collection);
      <C extends Collection<?>> Expression<Integer> size(C collection);
}

문자열 함수

public interface CriteriaBuilder {
    Expression<String> concat(Expression<String> x, Expression<String> y);
    Expression<String> concat(Expression<String> x, String y);
    Expression<String> concat(String x, Expression<String> y);
    Expression<String> substring(Expression<String> x, Expression<Integer> from);
    Expression<String> substring(Expression<String> x, int from);
    Expression<String> substring(Expression<String> x, Expression<Integer> from, Expression<Integer> len);
    Expression<String> substring(Expression<String> x, int from, int len);
    public static enum Trimspec { LEADING, TRAILING, BOTH }
    Expression<String> trim(Expression<String> x);
    Expression<String> trim(Trimspec ts, Expression<String> x);
    Expression<String> trim(Expression<Character> t, Expression<String> x);
    Expression<String> trim(Trimspec ts, Expression<Character> t, Expression<String> x);
    Expression<String> trim(char t, Expression<String> x);
    Expression<String> trim(Trimspec ts, char t, Expression<String> x);
    Expression<String> lower(Expression<String> x);
    Expression<String> upper(Expression<String> x);
    Expression<Integer> length(Expression<String> x);
    Expression<Integer> locate(Expression<String> x, Expression<String> pattern);
}

수학함수

public interface CriteriaBuilder {
      <N extends Number> Expression<N> neg(Expression<N> x);
      <N extends Number> Expression<N> abs(Expression<N> x);
      <N extends Number> Expression<N> ceiling(Expression<N> x);
      <N extends Number> Expression<N> floor(Expression<N> x);
      <T extends Number> Expression<T> round(Expression<T> x, Integer n);
      Expression<Integer> mod(Expression<Integer> x, Expression<Integer> y);
      Expression<Integer> mod(Expression<Integer> x, Integer y);
      Expression<Integer> mod(Integer x, Expression<Integer> y);
      Expression<Double> sqrt(Expression<? extends Number> x);
      Expression<Double> exp(Expression<? extends Number> x);
      Expression<Double> ln(Expression<? extends Number> x);
      Expression<Double> power(Expression<? extends Number> x, Expression<? extends Number> y);
      Expression<Double> power(Expression<? extends Number> x, Number y);

}

마무리

Criteria Query에 대해서 간단하게 다루었다. Criteria Query가 표준이지만 실제로는 아직 설명이 부족하고 내용도 많이 부족한 글이지만, 여러문에게 도움이 되었으면 하네요. 모두 즐거운 코딩생활되세요. ospace

참고

[1] 최범균, JPA 프로그래밍 입문, 가메출판사, 2017

[2] 김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘, 2015

[3] Noel Rodríguez Calle, Criteria Queries with Spring Data, https://refactorizando.com/en/criteria-queries-with-spring-data/, 2023.07.19

[4] Spring Data JPA - Multiple EnableJpaRepositories, https://stackoverflow.com/questions/45663025/spring-data-jpa-multiple-enablejparepositories, 2017.08.14

[5] JPQL Language Reference, https://openjpa.apache.org/builds/1.2.0/apache-openjpa-1.2.0/docs/manual/jpa_langref.html, 2024.04.23

반응형