동적 쿼리 작성 : Criteria와 QueryDSL

뾰족머리삼돌이·2024년 9월 8일

Spring Data JPA

목록 보기
4/9

Criteria

JPA에서는 프로그래밍 방식으로 JPQL 쿼리를 작성할 수 있는 빌더 클래스 Criteria API를 지원한다.
이를 이용해 도메인 클래스에 대한 select, from, where 절을 정의할 수 있다.

JPA에서의 Criteria 관련 내용은 해당 문서를 참고하고, 이번엔 Spring Data JPA 에서의 Criteria API를 알아보자

Spring Data JPA에서의 Criteria API 사용은 공식 문서의 Specifications 탭에 소개되어있다.

public interface CustomerRepository extends CrudRepository<Customer, Long>, JpaSpecificationExecutor<Customer> {}

Repository에 JpaSpecificationExecutor 인터페이스를 상속함으로써 Criteria API를 사용할 수 있다.
쿼리 메서드의 Specification<T> 타입의 인수를 제공함으로써 적용시킬 수 있는데, 해당 인터페이스는 아래의 구조로 작성되어있다.

public interface Specification<T> {
  Predicate toPredicate(Root<T> root, CriteriaQuery<?> query,
            CriteriaBuilder builder);
}

toPredicate의 매개변수는 Root<T>, CriteriaQuery<?>, CriteriaBuilder 로 이루어져있다.

Root<T>는 Entity 클래스를 나타내고,
CriteriaQuery<?>selectorderBy절 등의 여러부분을 수정하는데 사용되며,
CriteriaBuilderwhere절을 작성하는데 사용된다.

CriteriaQuery<?> 는 원래 JPA에서 select, join, orderBy 등 여러 작업을 수행하는데 사용된다.
하지만, Spring Data JPA에서는 쿼리 메서드의 명칭을 통해 자동생성되는 쿼리가 우선시되므로 무시될 수 있다.

personRepository.findAll((root, query, criteriaBuilder) -> {
	log.info(root.toString());

	log.info(query.getSelection().toString());
	query.select(root.get("id"));
	return criteriaBuilder.isNotNull(root.get("lastname"));
});

예를들어, 위 코드에서는 select의 결과값이 id 속성만 읽어오도록 설정하고 있다.
하지만 메서드 명칭이 findAll이기 때문에 실제로 읽어오는 데이터는 모든 속성을 가져온다.

personRepository.findAll((root, query, criteriaBuilder) -> {
	log.info(root.toString());

	query.orderBy(criteriaBuilder.asc(root.get("id")));
	return criteriaBuilder.isNotNull(root.get("lastname"));
});

따라서, CriteriaQuery<?>는 자동생성되는 쿼리 이외에 districtorderBy 같은 부가적인 부분을 수정하는 용도로 사용할 수 있다.

Specification<T> 인터페이스는 where절을 구성하는데 그 목적을 가지고있다.

CriteriaQuery<?>JpaSpecificationExecutor<T> 인터페이스에 작성되어 있는 쿼리 메서드들을 참고하여 부가적인 부분을 추가해야하는 상황에 사용하자


public class CustomerSpecs {


  public static Specification<Customer> isLongTermCustomer() {
    return (root, query, builder) -> {
      LocalDate date = LocalDate.now().minusYears(2);
      return builder.lessThan(root.get(Customer_.createdAt), date);
    };
  }

  public static Specification<Customer> hasSalesOfMoreThan(MonetaryAmount value) {
    return (root, query, builder) -> {
      // build query here
    };
  }
}

여러 곳에 Criteria API를 사용해야하는 상황에서 매번 동일한 내용을 작성하는 것은 좋지않다.
위 예시와 같이 도메인 클래스에서 사용되는 Specification 명세를 공통으로 관리하여 사용하자.

EntityManager 활용 쿼리작성

만약, Spring Data JPA의 쿼리 메서드를 사용하지 않고 직접 쿼리를 작성하는 경우에는 EntityManager를 이용할 수 있다.

CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();

CriteriaQuery<Person> query = criteriaBuilder.createQuery(Person.class);
Root<Person> root = query.from(Person.class);
query.select(root);
query.where(root.get("firstname").isNotNull());
query.orderBy(criteriaBuilder.asc(root.get("lastname")));

TypedQuery<Person> result = entityManager.createQuery(query);
log.info(result.getResultList().toString());

위 예시코드와 같이 EntityManager를 주입받아 CriteriaBuilder를 생성하고,
CriteriaQuery<T>를 생성한 뒤, 쿼리를 작성하고 결과를 얻을 수 있다.

다만 이 방법이 직관적이지 않고, 불편하기 때문에 보통 QueryDSL을 사용하는 경우가 많다

select p1_0.id,p1_0.city,p1_0.street,p1_0.zip_code,p1_0.firstname,p1_0.lastname from person p1_0 where p1_0.firstname is not null order by p1_0.lastname

위 코드를 돌리면 이런 쿼리가 발생한다.

QueryDSL

QueryDSL은 HQL( Hibernate Query Language )를 타입 안전한 형식으로 유지하기 위해 탄생한 오픈소스 프레임워크다.
Hibernate는 JPA의 대표적인 구현체이기 때문에 Spring Data JPA에서도 해당 프레임워크를 사용하고 있다.

따라서, Spring Data JPA 환경에서 QueryDSL의 사용 또한 가능하다.
탄생배경에서 알 수 있듯이 문자열, 혹은 외부 XML 파일로 쿼리를 작성하는 것이 아니라 프로그래밍 방식으로 타입 안전하게 쿼리를 작성하는 것이 주 목적이다.

JPA에서 공식적으로 지원하는 Criteria API와 그 용도가 비슷하지만, 이쪽이 좀 더 직관적이고 편리하다.
하지만, 별도의 의존성 설정이 필요하다.

dependencies {

	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	// querydsl
	implementation 'com.querydsl:querydsl-jpa:5.1.0:jakarta'
	annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
	annotationProcessor "jakarta.persistence:jakarta.persistence-api"
	annotationProcessor "jakarta.annotation:jakarta.annotation-api"

	// ...
}

QueryDSL의 동작은 애노테이션 프로세서를 이용하여 조건에 해당하는 도메인 클래스를 탐색하고,
각 도메인 클래스에 대치되는 Q클래스를 생성하여 동적 쿼리를 작성한다.

따라서, 혹시라도 IDE의 애노테이션 프로세서 설정이 꺼져있다면 켜줘야만 동작한다.

Q클래스 생성을 위해 필요한 조건은 @Entity와 getter/setter와 기본생성자 다.

public interface PersonRepository extends JpaRepository<Person, UUID>, QuerydslPredicateExecutor<Person> {
}

QueryDSL에 대한 자세한 내용과 설명은 공식 문서를 참고하도록 하고, Spring Data JPA에서의 사용방법은 대상이 될 Repository에 QuerydslPredicateExecutor<T>를 상속하면 된다.

Predicate predicate = QPerson.person.firstname.lower().eq("first")
		.and(QPerson.person.lastname.lower().eq("last"));
personRepository.findAll(predicate);

이렇게 되면 해당 인터페이스 내의 쿼리메서드를 사용할 수 있게 되며, Predicate 타입의 인수를 전달할 수 있다.

JPAQueryFactory

Repository를 경유하지 않고, 모든 쿼리를 직접 작성하고 싶을땐 JPAQueryFactory를 이용할 수 있다.

@Configuration
public class JavaConfig {

    @Bean
    public JPAQueryFactory jpaQueryFactory(EntityManager entityManager){
        return new JPAQueryFactory(entityManager);
    }
}

Configuration 파일에 JPAQueryFactory를 Bean으로 등록하고, 이를 주입받아 아래와 같이 사용할 수 있다.

Predicate predicate = QPerson.person.firstname.lower().eq("first")
		.and(QPerson.person.lastname.lower().eq("last"));
Iterable<Person> resultWithRepository = personRepository.findAll(predicate);

List<Person> resultWithFactory = jpaQueryFactory
		.select(QPerson.person)
		.from(QPerson.person)
		.where(QPerson.person.firstname.lower().eq("first")
				.and(QPerson.person.lastname.lower().eq("last")))
		.fetch();


for (Person p : resultWithRepository) {
	log.info("resultWithRepository {}", p.toString());
}

for(Person p : resultWithFactory){
	log.info("resultWithFactory {}", p.toString());
}


출력되는 로그를 확인해보면 두 결과가 똑같은 것을 확인할 수 있다.

0개의 댓글