본문 바로가기
WEB/Spring JPA

Spring JPA Criteria API 소개와 예제

by 정권이 내 2024. 6. 7.

Spring JPA Criteria API 개념과 사용법

 

Spring Data JPA 동적 쿼리란

Spring Data JPA의 동적 쿼리란 런타임 시점에 조건에 따라 동적으로 생성되는 쿼리를 의미합니다. 즉, 고정된 쿼리가 아닌, 입력 파라미터나 조건에 따라 쿼리가 달라지는 것을 말합니다. 동적 쿼리는 다양한 검색 조건이나 필터링 요구사항에 대응하기 위해 사용됩니다.

예를 들어, 사용자로부터 여러 검색 조건을 받아 그 조건에 따라 데이터를 필터링하여 조회하는 경우를 생각해볼 수 있습니다. 동적 쿼리를 사용하면 사용자가 입력한 조건에 맞추어 쿼리를 조합할 수 있습니다.

 

Criteria API 소개

JPA Criteria API는 JPA 2.0에서 도입된 타입 안전한 쿼리 작성 방법입니다. 이는 SQL 쿼리를 문자열로 작성하는 대신, 자바 코드로 쿼리를 구성할 수 있게 하여 컴파일 시점에 오류를 발견할 수 있고, 리팩토링에 유리합니다. Criteria API를 사용하면 동적 쿼리를 쉽게 생성할 수 있습니다.

 

라이브러리 의존성 추가

2024년 6월 기준으로 가장 최신버전의 라이브러리를 적용하였습니다.

implementation 'org.hibernate:hibernate-entitymanager:5.4.32.Final'

 

Criteria API 구성요소

1. CriteriaBuilder

  • Criteria 쿼리를 작성하기 위한 팩토리 클래스로, EntityManager 로부터 얻을 수 있습니다.
CriteriaBuilder cb = entityManager.getCriteriaBuilder();

 

2. CriteriaQuery

  • 쿼리 자체를 나타내는 객체로, CriteriaBuilder 를 사용해 생성합니다.
CriteriaQuery<User> cq = cb.createQuery(User.class);

 

3. Root

  • 쿼리의 FROM 절을 나타내며, 조회할 엔티티를 지정합니다.
Root<User> user = cq.from(User.class);

 

4. Predicate

  • WHERE 절의 조건을 나타내는 객체로, 여러 조건을 결합할 수 있습니다.
Predicate condition = cb.equal(user.get("name"), "John");
cq.where(condition);

 

  • List로 만들어서 여러개의 조건을 사용할수도 있습니다.
List<Predicate> predicates = new ArrayList<>();
predicates.add(cb.equal(user.get("name"), "John"));
predicates.add(cb.like(user.get("mbti"), "%" + value + "%"));

 

Criteria API 사용 예제

gradle dependency 추가

implementation 'org.hibernate:hibernate-entitymanager:5.4.32.Final'

 

Bean 객체 주입 (EntityManager)

Spring Bean으로 등록된 EntityManager를 주입받아야 Criteria 관련 클래스들을 사용할수 있습니다.

@RequiredArgsConstructor
public class UserService() {
    
	private final EntityManager entityManager;

	public void getUser(){
		CriteriaBuilder cb = entityManager.getCriteriaBuilder();
		CriteriaQuery<User> cq = cb.createQuery(User.class);
		Root<User> user = cq.from(User.class);
		...
    }
}

 

SELECT 예제

CriteriaBuilder는 다양한 선택 항목을 생성할 수 있습니다. 단일 필드, 엔티티, 복합 필드 등을 선택할 수 있습니다.

public List<User> findUsersByCriteria(String name) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);

    if (name == null) {
        cq.select(user); // 전체 엔티티 선택            
    } else {
        cq.select(user.get("name")); // 특정 필드 선택
    }

    return entityManager.createQuery(cq).getResultList();
}
  • 위 예제는 메서드의 파라미터로 받은 이름을 가진 사용자를 조회하는 메서드입니다. name 변수가 null일 경우 모든 엔티티를 조회하고 null이 아닐경우 "name" 이라는 컬럼만 가져오는 예제입니다.

 

WHERE 예제

CriteriaBuilder는 다양한 조건을 표현하는 Predicate 객체를 생성할 수 있습니다. 이들은 CriteriaQuery의 where 메서드에 전달되어 쿼리의 조건을 정의합니다.

public List<User> findUsersByCriteria(String name, Integer age) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);

    List<Predicate> predicates = new ArrayList<>();

    if (name != null)
        predicates.add(cb.equal(user.get("name"), name));

    if (age != null)
        predicates.add(cb.equal(user.get("age"), age));

    cq.where(predicates.toArray(new Predicate[0]));

    return entityManager.createQuery(cq).getResultList();
}
  • name, age 값의 null 여부에 따라 각각 "name", "age" 필드에 대해 메서드의 인자값으로 받은 name, age와 일치여부를 비교하여 조건절에 추가하는 예제입니다.

 

사용 가능한 비교/논리 연산자

  • equal: 두 값이 같은지 비교
  • notEqual: 두 값이 다른지 비교
  • greaterThan, greaterThanOrEqualTo: 값이 크거나 같은지 비교
  • lessThan, lessThanOrEqualTo: 값이 작거나 같은지 비교
  • and: 여러 조건을 AND로 결합
  • or: 여러 조건을 OR로 결합
  • not: 조건을 반대로 만듦

 

ORDER 예제

CriteriaBuilder는 정렬 조건을 생성할 수 있습니다. asc, desc 메서드를 사용하여 오름차순 또는 내림차순 정렬을 정의합니다.

public List<User> findUsersByCriteria() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);

    cq.select(user); // 모든 필드 선택
    cq.orderBy(cb.asc(user.get("name"))); // 이름 오름차순 정렬
    cq.orderBy(cb.desc(user.get("age"))); // 나이 내림차순 정렬    
    
    return entityManager.createQuery(cq).getResultList();
}
  • "name" 필드에 대해 오름차순, "age" 필드에 대해 내림차순으로 쿼리를 실행하는 예제입니다.

 

GROUP BY 예제

public List<User> findUsersByCriteria() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);

    cq.select(user.get("department")); // 모든 필드 선택
    cq.groupBy(user.get("department"));
    
    return entityManager.createQuery(cq).getResultList();
}
  • "department" 단일 필드를 조회하며 해당 필드에 대해 그룹화를 적용하는 예제입니다.

 

JOIN 예제

public List<User> findUsersByCriteria() {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);
    
    Join<User, Department> department = user.join("department");
    cq.select(user).where(cb.equal(department.get("name"), "IT"));
    
    cq.select(user.get("department")); // 모든 필드 선택
    cq.groupBy(user.get("department"));
    
    return entityManager.createQuery(cq).getResultList();
}
  • Root로 지정한 엔티티에 조인할 엔티티는 Root 객체의 join 메서드로 지정할수 있습니다.

 

페이징, 정렬 예제

동적 조건 검색과 페이징을 함께 구현하는 방법을 보여줍니다. UserRepository 클래스에서 특정 조건에 맞는 User 엔티티를 조회하고, 페이징 결과를 반환하는 메서드를 구현합니다.

public Page<User> findUsersByCriteria(String name, Integer age, Pageable pageable) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<User> cq = cb.createQuery(User.class);
    Root<User> user = cq.from(User.class);
    List<Predicate> predicates = new ArrayList<>();

    cq.select(user.get("name"));

    // 정렬조건 추가
    if (pageable.getSort().isSorted()) {
        pageable.getSort().forEach(order -> {
            cq.orderBy(order.isAscending() ? cb.asc(user.get(order.getProperty())) : cb.desc(user.get(order.getProperty())));
        });
    }

    // 페이징 설정
    TypedQuery<User> query = entityManager.createQuery(cq);
    query.setFirstResult((int) pageable.getOffset());
    query.setMaxResults(pageable.getPageSize());

    List<User> resultList = query.getResultList();
    long total = getTotalCount(predicates);

    return new PageImpl<>(resultList, pageable, total);
}

private long getTotalCount(List<Predicate> predicates) {
    CriteriaBuilder cb = entityManager.getCriteriaBuilder();
    CriteriaQuery<Long> countQuery = cb.createQuery(Long.class);
    Root<User> userRoot = countQuery.from(User.class);

    countQuery.select(cb.count(userRoot)).where(predicates.toArray(new Predicate[0]));
    return entityManager.createQuery(countQuery).getSingleResult();
}

페이징, 정렬 예제는 다른 예제들에 비해 복잡하므로 순서와 함께 설명하겠습니다.

  1. EntityManager 의존성 주입을 받고 Criteria API 객체들을 선언합니다.
  2. 메서드는 검색 조건(name, age)과 페이징 정보를 담은 Pageable 객체를 매개변수로 받습니다.
  3. 검색 조건(name과 age)에 따라 Predicate 객체를 생성하고, 리스트에 추가합니다.
  4. Pageable 객체에 정의된 정렬 조건을 CriteriaQuery에 추가합니다.
  5. TypedQuery 생성 및 페이징 설정을 합니다.

 

TypeQuery 개념

TypedQuery 는 JPA 에서 사용되는 인터페이스로, 특정 타입의 쿼리 결과를 처리하기 위해 사용됩니다. EntityManager의 createQuery 메서드에 의해 생성되며, 쿼리의 결과를 특정 엔티티 타입이나 값 타입으로 안전하게 캐스팅할 수 있습니다.

  • 타입 안전성 제공: TypedQuery는 쿼리 결과의 타입을 지정하기 때문에 컴파일 시 타입 안전성을 보장합니다. 이는 결과를 처리할 때 발생할 수 있는 ClassCastException을 방지하는 데 도움이 됩니다.
  • 쿼리 실행: TypedQuery는 다양한 쿼리 실행 메서드를 제공하여 쿼리 결과를 가져옵니다. 예를 들어, 결과 리스트를 반환하거나 단일 결과를 반환할 수 있습니다.
  • 파라미터 설정: TypedQuery는 파라미터를 설정하는 메서드를 제공하여 동적 쿼리를 구성할 수 있습니다. 이는 쿼리 실행 시 필요한 조건을 동적으로 변경할 수 있도록 합니다.
  • 페이징 지원: TypedQuery는 결과를 페이징 처리하는 메서드를 제공합니다. 이는 대량의 데이터를 효율적으로 처리하는 데 유용합니다.

 

TypeQuery 주요 메서드

  • getResultList(): 쿼리의 결과를 리스트로 반환합니다.
  • getSingleResult(): 쿼리의 단일 결과를 반환합니다. 결과가 없거나 여러 개일 경우 예외가 발생합니다.
  • setParameter(String name, Object value): 명명된 파라미터를 설정합니다.
  • setParameter(int position, Object value): 위치 기반 파라미터를 설정합니다.
  • setFirstResult(int startPosition): 첫 번째 결과의 위치를 설정하여 페이징 처리를 지원합니다.
  • setMaxResults(int maxResult): 최대 결과 개수를 설정하여 페이징 처리를 지원합니다.

 

결론

CriteriaBuilder는 JPA에서 타입 안전하고 동적인 쿼리를 작성하는 강력한 도구입니다. 다양한 쿼리 구성 요소를 제공하여 복잡한 쿼리도 자바 코드로 쉽게 표현할 수 있습니다. 이를 통해 개발자는 동적 조건과 정렬, 페이징, 조인, 집계 등의 기능을 유연하게 구현할 수 있습니다.

반응형

댓글