쿼리에 대한 도메인 특화 언어로 Java 진영에서 데이터 엑세스 할 때 사용할 수 있는 프레임워크이다.
QueryDSL을 사용하면 다음과 같은 장점을 누릴 수 있다.
- IDE에서 코드 완성을 지원한다.
- 문법적으로 잘못된 쿼리를 허용하지 않는다.
- 도메인에 유형 및 속성을 안전하게 참조할 수 있다.
- 도메인에 변경이 발생할 때 리팩토링하는데 비교적 쉽게 대응이 가능하다.
위에 장점들을 설명하면 JPA NativeQuery나 MyBatis와 같은 쿼리를 직접 다루는 코드들은 컴파일 에러를 발생하지 않지만 QueyrDSL은 컴파일 단계에서 코드에 오류를 잡아낼 수 있다는 장점을 가진다.
스프링 버전에 따라 설정법이 약간 다른데 Spring 3.x.x 이상, Kotlin, Gradle을 이용한 방법이다.
plugins {
...
// Kotlin Annotation Processing Tool
id 'org.jetbrains.kotlin.kapt' version '1.8.22'
// Intellij에서 사용할 파일 생성 플로그인
id 'idea'
}
...
dependencies {
...
// queryDsl start
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
kapt "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
kapt "jakarta.annotation:jakarta.annotation-api"
kapt "jakarta.persistence:jakarta.persistence-api"
}
// 생성 Q파일 경로 설정
idea {
module {
def kaptMain = file("build/generated/source/kapt/main")
sourceDirs.add(kaptMain)
generatedSourceDirs.add(kaptMain)
}
}
제대로 반영되었는지는 JPA 엔티티가 있을 경우 빌드 시에 지정한 경로에 엔티티명 앞에 Q가 붙은 QClass가 생성되는지 확인해보면 된다.
QueryDSL은 QueryFactory를 통해 Query라는 객체를 생성해 SQL을 작성할 수 있다. 내가 사용할 JPA을 지원하기 위해서는 JpaQueryFactory를 생성하고 JpaQuery를 통해 사용할 수 있다.
아래는 JpaQueryFactory를 빈으로 설정하는 방법이다.
@Configuration
class QueryDslConfig(
private val entityManager: EntityManager,
) {
@Bean
fun jpaQueryFactory() = JPAQueryFactory(entityManager)
}
위와 같이 빈으로 등록된 JpaQueryFactory을 이용해 아래와 같이 QueryDSL을 사용할 수 있다.
@Repository
class CustomerQueryRepository(
private val jpaQueryFactory: JPAQueryFactory,
) {
private val customer = QCustomer("qCustomer")
fun findByFistName(firstName: String): Optional<Customer> {
val result = jpaQueryFactory.selectFrom(qCustomer)
.where(qCustomer.firstName.eq(firstName))
.fetchOne()
return Optional.ofNullable(result)
}
}
먼저 주목해야할 것은 QCustomer이다. Customer라는 도메인 유형을 QueryDSL에서 사용할 대표 클래스를 의미한다. 이러한 대표 Q클래스는 두 가지 방법으로 생성할 수 있다.
private val customer = QCustomer.customer //정적 인스턴스
private val customer = QCustomer("qCustomer") //고유 인스턴스
일반적으로 고유 인스턴스인 QCustomer("qCustomer")을 사용하는 거 같다.
다음으로 jpaQueryFactory를 이용해서 jpaQuery를 작성하는데 Sql에서 사용하는 것 처럼
select(QClass), From(QClass), selectFrom(QClass)와 같이 사용하며 where() 절을 통해 조건을 만들 수 있다.
마지막에 있는 fetchOne()이라는 문법이 있는데 QueryDSL에게 결과를 반환하라는 의미이다. 이 외에도 fetchXXX()와 같은 문법들이 있으니 참고하자.
이 외에도 orderBy(), groupBy(), limit(), join() 과 같은 문법을 제공한다.
간단한 조회는 이 정도로 알아보고 응용 조회는 아래에서 별도로 알아보자.
QueryDSL도 쿼리를 이용하는거기에 당연히 CRUD 기능이 모두 지원된다. 다만 개인적으로 조회 외에는 JPA가 있기에 굳이 사용해야 되나 싶다.
혹시 모르니 간단하게 예제 코드만 적어 두자
fun deleteAll() {
jpaQueryFactory.delete(qCustomer).execute()
}
fun update(beforeFirstName: String, afterFirstName: String) {
jpaQueryFactory.update(qCustomer)
.where(qCustomer.firstName.eq(beforeFirstName))
.set(qCustomer.firstName, afterFirstName)
.execute()
}
val qChildCat = QCat("qChildCat")
val qParentCat = QCat("qParentCat")
fun findByMaxParentAge(): Optional<Cat> {
val result = jpaQueryFactory.selectFrom(qChildCat)
.innerJoin(qChildCat.parentCat, qParentCat)
.where(
qParentCat.age.eq(
JPAExpressions.select(qParentCat.age.max()).from(qParentCat)
)
)
.fetchOne()
return Optional.ofNullable(result)
}
위에 예제를 보면 where절에서 사용한 JPAExpressions 객체는 QueryDSL JPA 모듈에서 JPAQuery를 생성하는 유틸리티 클래스이다.
해당 객체는 서브쿼리용 JPAQuery라고 생각하면 되고 JPAQuery와 같은 문법을 사용한다는 특징이 있다.
따라서 위에 코드를 해석하면 자식고양이들 중 부모고양이에 나이가 가장 많은 고양이를 찾는 예제이다.(나이가 같은 고양이는 없다라고 가장한 코드이다.)