Spring Data JPA - QueryDSL With Kotlin, Gradle

Spring Data JPA

백기선님의 강의인 Spring Data JPA 강의를 듣고 공부한 내용을 정리한 글

QueryDSL을 사용하는 이유

Spring Data Common에서 QueryDSL을 사용해보자.

자바 코드로 조건문을 표현할 수 있으며, 조건문을 조합할 수도 있다.

가령 아래와 같은 메소드가 있다고 상상해보라

fun findByFirstNameIngnoreCaseAndLastNameStartsWithIgnoreCase(firstName:String, lastName:String)

상상만해도 끔찍한 함수다.

차라리 한글로 주석을 다는게 나을 정도다.

지금부터 배울 QueryDSL을 사용하면 조금 더 간단하게 조건을 표현할 수 있게 된다.

QueryDSL의 종류

쿼리 메소드의 종류가 많기는 하지만 대부분 두 가지 중 하나다.

findOne(predicate): Optional<T> 이런 저런 조건으로 무언가 하나를 찾는다.

findAll(predicate): List<T>,Page<T> 이런 저런 조건으로 무언가 여러개를 찾는다. (페이징을 한다면 Page 클래스 타입 사용)

레포지토리에 메소드를 늘리지 않고도 다양한 리포지토리 쿼리를 사용할 수 있게 된다.

QueryDSL은 JPA 뿐만 아니라 다른 Mongodb, 루씬같은 모듈들도 지원한다.

Kotlin + QueryDSL + Gradle(KotlinDSL)

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    val kotlinVersion = "1.3.72"
    val springBootVersion = "2.3.0.RELEASE"
    id("org.springframework.boot") version springBootVersion
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    kotlin("jvm") version kotlinVersion
    kotlin("plugin.spring") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.jpa") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.allopen") version kotlinVersion
    id("org.jetbrains.kotlin.plugin.noarg") version kotlinVersion
    id("com.ewerk.gradle.plugins.querydsl") version "1.0.10"
    kotlin("kapt") version kotlinVersion
}

sourceSets["main"].withConvention(org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet::class) {
    kotlin.srcDir("$buildDir/generated/source/kapt/main")
}

group = "me.harry"
version = "0.0.1"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation(group = "org.javassist", name = "javassist", version = "3.27.0-GA")
    implementation(group = "com.querydsl", name = "querydsl-apt", version = "4.1.0")
    implementation ("com.querydsl:querydsl-jpa")
    kapt("org.springframework.boot:spring-boot-configuration-processor")
    kapt("com.querydsl:querydsl-apt:4.2.1:jpa")
    runtimeOnly ("com.h2database:h2")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "11"
    }
}

실습에 앞서 내가 사용하는 의존성 패키지 구조이다.

혹시나 이 글을 보는 사람이 백기선님의 강의를 보다가

어 기선님은 maven 쓰시네? 나는 Gradle에 Kotlin인데 오또카징?라는 상황이 생긴다면

내 의존성을 참고하길 바란다.

스크린샷 2020-06-23 오후 11 03 41

의존성 추가 후 빌드를 해보면 Build 폴더 아래에 다음과 같이 Entity들이 QClass로 생성된 것을 확인할 수 있다. (여기까지 나오면 셋팅이 잘 된 것)

프로젝트에 Entity들이 많은데 이번 실습때 사용할 것은 딱 하나 Account 도메인을 사용할 것이다.


@Entity
class Account(
    @Id @GeneratedValue
    val id:Long?,
    // @Column이 생략되어 있음
    var username:String,
    val password:String,
    val firstName:String,
    val lastName:String,

    @Embedded
    // kotlin에선 AttributeOverrides 안써도 됨
    @AttributeOverride(name = "street", column = Column(name = "home_street"))
    val address:Address?
){
    fun addStudy(study: Study) {
        this.studies.add(study)
        study.owner = this
    }

    fun removeStudy(study: Study) {
        this.studies.remove(study)
        study.owner = this
    }

    @OneToMany(mappedBy = "owner")
    val studies:MutableSet<Study> = HashSet()
}
interface AccountRepository: JpaRepository<Account,Long>, QuerydslPredicateExecutor<Account> {

}

그리고 AccountRepository에 QuerydslPredicateExecutor 인터페이스를 추가하자.

이 인터페이스가 제공하는 두 개(?)의 메소드를 사용하자.

그리고 이제 predicate를 사용한 QueryDSL의 위력을 맛볼 시간이다.

테스트 코드를 작성하자!

@DataJpaTest
class AccountRepositoryTest {

    @Autowired
    lateinit var accountRepository: AccountRepository

    @Test
    fun crud() {
        val account = QAccount.account
        val predicate: Predicate = account.firstName.containsIgnoreCase("harry")
                .and(account.lastName.startsWith("code"))

        val findOne = accountRepository.findOne(predicate)
        assertThat(findOne).isEmpty
    }
}

queryDSL을 사용해서 마치 코딩을 하듯이 코틀린 코드로 query 문을 작성했다.

현재 테스트 DB인 h2 DB에는 아무런 값도 없기 때문에 findOne을 해봤자 빈 값이 오게 될 것이다.

그럼 쿼리가 우리가 의도한대로 날아갔는지 확인해보기 위해 하이버네이트가 작성한 쿼리를 눈으로 확인해보자.

Hibernate: 
    select
        account0_.id as id1_0_,
        account0_.city as city2_0_,
        account0_.state as state3_0_,
        account0_.home_street as home_str4_0_,
        account0_.zip_code as zip_code5_0_,
        account0_.first_name as first_na6_0_,
        account0_.last_name as last_nam7_0_,
        account0_.password as password8_0_,
        account0_.username as username9_0_ 
    from
        account account0_ 
    where
        (
            lower(account0_.first_name) like ? escape '!'
        ) 
        and (
            account0_.last_name like ? escape '!'
        )

우리가 의도한 대로 잘 날아간 것을 확인할 수 있다.

first_name에 lower를 붙인 이유는 잘모르겠지만 어쨌든 잘 검색하고 있는 것을 확인할 수 있다.

Reference

인프런 백기선님의 스프링 Data JPA



© 2022. by minkuk

Powered by minkuk