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인데 오또카징?라는 상황이 생긴다면
내 의존성을 참고하길 바란다.
의존성 추가 후 빌드를 해보면 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