- 이번 포스팅에서는 Kotlin 언어로 구성된 Springboot 에서 QueryDSL 을 설정하는 방법에서부터 테스트까지 진행하겠습니다.
- 설정
build.gradle.kts 파일 안에,
plugins {
kotlin("jvm") version "1.9.25"
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.3.4"
id("io.spring.dependency-management") version "1.1.6"
// 추가
kotlin("plugin.allopen") version "2.0.21" // allOpen 에 지정한 어노테이션으로 만든 클래스에 open 키워드를 적용
kotlin("plugin.noarg") version "2.0.21" // noArg 에 지정한 어노테이션으로 만든 클래스에 자동으로 no-arg 생성자를 생성
// QueryDSL Kapt
kotlin("kapt")
}
group = "raillylinker.module_idp_jpa"
version = "0.0.1-SNAPSHOT"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
// (기본)
implementation("org.springframework.boot:spring-boot-starter:3.3.4")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.21")
testImplementation("org.springframework.boot:spring-boot-starter-test:3.3.4")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit5:2.0.21")
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.11.2")
// (JPA)
// : DB ORM
api("org.springframework.boot:spring-boot-starter-data-jpa:3.3.4")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-hibernate5:2.18.0")
implementation("org.hibernate:hibernate-validator:8.0.1.Final")
implementation("com.mysql:mysql-connector-j:9.0.0") // MySQL
// (QueryDSL)
implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta")
kapt("com.querydsl:querydsl-apt:5.1.0:jakarta")
kapt("jakarta.annotation:jakarta.annotation-api")
kapt("jakarta.persistence:jakarta.persistence-api")
}
// (Querydsl 설정부 추가 - start)
val generated = file("src/main/generated")
// querydsl QClass 파일 생성 위치를 지정
tasks.withType<JavaCompile> {
options.generatedSourceOutputDirectory.set(generated)
}
// kotlin source set 에 querydsl QClass 위치 추가
sourceSets {
main {
kotlin.srcDirs += generated
}
}
// gradle clean 시에 QClass 디렉토리 삭제
tasks.named("clean") {
doLast {
generated.deleteRecursively()
}
}
kapt {
generateStubs = true
}
// (Querydsl 설정부 추가 - end)
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
}
}
tasks.withType<Test> {
useJUnitPlatform()
}
// kotlin jpa : 아래의 어노테이션 클래스에 no-arg 생성자를 생성
noArg {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
// kotlin jpa : 아래의 어노테이션 클래스를 open class 로 자동 설정
allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}
위와 같이 설정합니다.
일반적인 JPA 설정과 다른점으로는,
plugins 블록 안의
kotlin("kapt")
와,
dependencies 블록 안의
// (QueryDSL)
implementation("com.querydsl:querydsl-jpa:5.1.0:jakarta")
kapt("com.querydsl:querydsl-apt:5.1.0:jakarta")
kapt("jakarta.annotation:jakarta.annotation-api")
kapt("jakarta.persistence:jakarta.persistence-api")
그리고,
// (Querydsl 설정부 추가 - start)
val generated = file("src/main/generated")
// querydsl QClass 파일 생성 위치를 지정
tasks.withType<JavaCompile> {
options.generatedSourceOutputDirectory.set(generated)
}
// kotlin source set 에 querydsl QClass 위치 추가
sourceSets {
main {
kotlin.srcDirs += generated
}
}
// gradle clean 시에 QClass 디렉토리 삭제
tasks.named("clean") {
doLast {
generated.deleteRecursively()
}
}
kapt {
generateStubs = true
}
// (Querydsl 설정부 추가 - end)
위와 같은 부분이 추가된 것입니다.
저의 경우는 모듈 구조이므로 위와 같이 kotlin("kapt") 에 버전을 설정하지 않았는데,
정식으로는,
kotlin("kapt") version "2.0.21"
위와 같이 버전을 설정해주면 됩니다.
다음으로, @Configuration 빈을 추가할 것인데,
import com.querydsl.jpa.impl.JPAQueryFactory
import jakarta.persistence.EntityManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@Configuration
class QueryDslConfig(
val em: EntityManager
) {
@Bean
fun queryFactory(): JPAQueryFactory {
return JPAQueryFactory(em)
}
}
이렇게 추가하면 끝입니다.
이 상태에서 Gradle.build 를 하면,
위와 같이 build 된 결과물로, JPA 의 Entity 클래스 이름 앞에 Q 가 달린 DSL 파일이 생성되는 것을 볼 수 있습니다.
- 테스트
기능이 제대로 동작하는지 테스트를 하겠습니다.
jpa entity 로, 외례키가 지정된 두 entity 를 만들고 테스트할 것인데,
Db1_Template_FkTestManyToOneChild.kt
import jakarta.persistence.*
import org.hibernate.annotations.Comment
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.time.LocalDateTime
// Fk 관계 중 OneToOne 은 논리적 삭제를 적용하는 본 프로젝트에서 사용이 불가능합니다.
// 고로, One to One 역시 Many to One 을 사용하며,
// 로직상으로 활성화된 행이 한개 뿐이라고 처리하면 됩니다. (합성 Unique 로 FK 변수를 유니크 처리하면 더 좋습니다.)
// 주의 : 낙관적 Lock (@Version) 사용시 Transaction 기능과 충돌이 있음
@Entity
@Table(
name = "fk_test_many_to_one_child",
catalog = "template"
)
@Comment("Foreign Key 테스트용 테이블 (one to many 테스트용 자식 테이블)")
class Db1_Template_FkTestManyToOneChild(
@Column(name = "child_name", nullable = false, columnDefinition = "VARCHAR(255)")
@Comment("자식 테이블 이름")
var childName: String,
@ManyToOne
@JoinColumn(name = "fk_test_parent_uid", nullable = false)
@Comment("FK 부모 테이블 고유번호 (template.fk_test_parent.uid)")
var fkTestParent: Db1_Template_FkTestParent
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "uid", columnDefinition = "BIGINT UNSIGNED")
@Comment("행 고유값")
var uid: Long? = null
@Column(name = "row_create_date", nullable = false, columnDefinition = "DATETIME(3)")
@CreationTimestamp
@Comment("행 생성일")
var rowCreateDate: LocalDateTime? = null
@Column(name = "row_update_date", nullable = false, columnDefinition = "DATETIME(3)")
@UpdateTimestamp
@Comment("행 수정일")
var rowUpdateDate: LocalDateTime? = null
// ---------------------------------------------------------------------------------------------
// <중첩 클래스 공간>
}
Db1_Template_FkTestParent.kt
import jakarta.persistence.*
import org.hibernate.annotations.Comment
import org.hibernate.annotations.CreationTimestamp
import org.hibernate.annotations.UpdateTimestamp
import java.time.LocalDateTime
// 주의 : 낙관적 Lock (@Version) 사용시 Transaction 기능과 충돌이 있음
@Entity
@Table(
name = "fk_test_parent",
catalog = "template"
)
@Comment("Foreign Key 테스트용 테이블 (부모 테이블)")
class Db1_Template_FkTestParent(
@Column(name = "parent_name", nullable = false, columnDefinition = "VARCHAR(255)")
@Comment("부모 테이블 이름")
var parentName: String
) {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "uid", columnDefinition = "BIGINT UNSIGNED")
@Comment("행 고유값")
var uid: Long? = null
@Column(name = "row_create_date", nullable = false, columnDefinition = "DATETIME(3)")
@CreationTimestamp
@Comment("행 생성일")
var rowCreateDate: LocalDateTime? = null
@Column(name = "row_update_date", nullable = false, columnDefinition = "DATETIME(3)")
@UpdateTimestamp
@Comment("행 수정일")
var rowUpdateDate: LocalDateTime? = null
@OneToMany(
// mappedBy 는 자식 테이블 클래스의 Join 정보를 나타내는 변수명을 적어주면 됩니다. (변수명이 다르면 에러가 납니다.)
// Fk 제약은 mappedBy 를 한 대상 테이블에 생성됩니다.
mappedBy = "fkTestParent",
// 이것에 해당하는 정보는 아래 변수를 get 했을 시점에 데이터베이스에서 가져오도록 설정
fetch = FetchType.LAZY,
/*
본 부모 테이블이 삭제 등 변경 되었을 때 아래 자식 테이블들에 대한 처리 방침.
CascadeType.ALL 설정이 된 상태로 본 부모 테이블이 삭제되면 해당 설정이 달린 자식 테이블들이 모두 삭제됩니다.
외례키가 테이블 존재에 필수적인 경우는 cascade 설정을 해도 되지만,
아니라면 아래 설정을 적용하지 말고, 삭제시에는 수동으로 기본값으로 변경을 수행해주어야 합니다.
ex : 회원 테이블이 대표글 정보를 저장하기 위하여 글 테이블을 참조하고 있을 때,
cascade 설정이 되어있다면, 그저 글을 하나 지웠을 뿐인데 회원 정보가 날아가는 상황이 벌어집니다.
만약 cascade 설정이 안 된 상태로 자식 테이블이 존재하는 부모 테이블만 제거하려 할 때엔 무결성 에러가 발생합니다.
*/
cascade = [CascadeType.ALL]
)
var fkTestManyToOneChildList: MutableList<Db1_Template_FkTestManyToOneChild> = mutableListOf()
// ---------------------------------------------------------------------------------------------
// <중첩 클래스 공간>
}
위와 같이 Entity 가 존재하고,
이에대한 QueryDsl Repository 로,
import com.querydsl.jpa.impl.JPAQueryFactory
import jakarta.persistence.EntityManager
import org.springframework.stereotype.Repository
import raillylinker.module_idp_jpa.data_sources.database_jpa.db1_main.entities.Db1_Template_FkTestManyToOneChild
import raillylinker.module_idp_jpa.data_sources.database_jpa.db1_main.entities.Db1_Template_FkTestParent
import raillylinker.module_idp_jpa.data_sources.database_jpa.db1_main.entities.QDb1_Template_FkTestManyToOneChild.db1_Template_FkTestManyToOneChild
import raillylinker.module_idp_jpa.data_sources.database_jpa.db1_main.entities.QDb1_Template_FkTestParent.db1_Template_FkTestParent
@Repository
class FkTestRepository(
entityManager: EntityManager
) {
private val queryFactory: JPAQueryFactory = JPAQueryFactory(entityManager)
// 부모 테이블과 자식 테이블을 조인하여 조회하는 예시
fun findParentWithChildren(): List<Db1_Template_FkTestParent> {
return queryFactory
.selectFrom(db1_Template_FkTestParent)
.leftJoin(db1_Template_FkTestParent.fkTestManyToOneChildList, db1_Template_FkTestManyToOneChild)
.fetchJoin() // fetchJoin을 사용하여 자식 엔티티를 함께 가져옴
.fetch() // 결과를 가져옴
}
// 특정 조건으로 부모-자식 조회 (예: 부모 이름으로 필터링)
fun findParentWithChildrenByName(parentName: String): List<Db1_Template_FkTestParent> {
return queryFactory
.selectFrom(db1_Template_FkTestParent)
.leftJoin(db1_Template_FkTestParent.fkTestManyToOneChildList, db1_Template_FkTestManyToOneChild)
.fetchJoin()
.where(db1_Template_FkTestParent.parentName.eq(parentName))
.fetch()
}
// 부모-자식 테이블의 특정 자식 데이터 조회
fun findChildByParentId(parentId: Long): List<Db1_Template_FkTestManyToOneChild> {
return queryFactory
.selectFrom(db1_Template_FkTestManyToOneChild)
.leftJoin(db1_Template_FkTestManyToOneChild.fkTestParent, db1_Template_FkTestParent)
.where(db1_Template_FkTestParent.uid.eq(parentId))
.fetch()
}
}
이렇게 작성하였습니다.
테스트할 Service 클래스에,
private val fkTestRepository : FkTestRepository
위 변수를 주입받고,
val result = fkTestRepository.findParentWithChildren()
for(f in result){
println(f.uid)
}
위 코드를 실행시키면,
제가 이전에 넣어둔 데이터가 잘 반환되는 것을 볼 수 있습니다.
- 이상입니다.
'Springboot' 카테고리의 다른 글
Springboot Kafka Json Value 매핑하기 (3) | 2024.10.28 |
---|---|
Springboot 서버 비동기 처리 - Redis 를 이용한 분산락 설명 및 구현 (0) | 2024.10.21 |
Springboot logback 적용 (0) | 2024.10.16 |
Springboot kotlin 멀티모듈 구조 적용하기 (기능-서비스 단위 모듈화, 멀티 프로필 application.yml, 모듈간 종속 관계, 모듈간 Bean 주입, Kotlin 사용) (3) | 2024.10.15 |
Springboot 로 Socket(STOMP) 개발하기 (1) | 2024.10.14 |