Kotlin + SpringBoot에 JOOQ 적용기 - 2. 쿼리 사용해보기

저번 글에서, Kotlin DSL로 JOOQ를 프로젝트에 적용시켜봤다.
2025.02.28 - [개발/SPRING] - Kotlin + SpringBoot에 JOOQ 적용기 - 1. 선택 이유와 코드 자동 생성하기
사실 사용법은 여기서 다룰 필요 없이 공식문서에서 굉장히 잘 다뤄주고 있다.
https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/

이번 포스팅에서는 그냥 내가 썼던 쿼리들을 몇개 가져와서 어떻게 썼나 소개해보려고 한다.
SELECT
fun findById(userId: String, projectId: String): Project {
return dsl.selectFrom(PROJECTS)
.where(PROJECTS.ID.eq(projectId)
.and(PROJECTS.CREATOR_ID.eq(userId)))
.fetchOneInto(Project::class.java)
?: throw NoSuchElementException("Project not found for given user.")
}
fun findByIds(userId: String, projectIds: List<String>): List<Project> {
return dsl.selectFrom(PROJECTS)
.where(PROJECTS.ID.`in`(projectIds)
.and(PROJECTS.CREATOR_ID.eq(userId)))
.fetchInto(Project::class.java)
}
PROJECT는 내가 커스텀하게 만든 객체인데 별다른 조치 없이 바로 매핑이된다. 물론 내가 조회한 데이터는 변수로 모두 들고 있어야한다.
SELECT COUNT
fun countById(userId: String, projectId: String): Boolean {
return dsl.selectCount()
.from(PROJECTS)
.where(PROJECTS.ID.eq(projectId)
.and(PROJECTS.CREATOR_ID.eq(userId)))
.fetchOne(0, Int::class.java) ?: 0
}
SELECT EXIST
fun existsNameByUserId(userId: String, name: String): Boolean {
return dsl.fetchExists(
dsl.selectOne()
.from(PROJECTS)
.where(
PROJECTS.CREATOR_ID.eq(userId)
.and(PROJECTS.NAME.eq(name))
)
)
}
이정도 쿼리들은 JPA로 바로 만들어져서 JPA와 혼용으로 쓰는게 좋겠다는 생각이 들었다. 간단한 SELECT 쿼리들만봐도 DSL이지만 비교적 퀄와 유사하다는걸 느낄 수 있었다.
INSERT
fun create(project: Project, id: String) {
dsl.insertInto(PROJECTS)
.set(PROJECTS.ID, id)
.set(PROJECTS.NAME, project.name)
.set(PROJECTS.CREATOR_ID, project.creatorId)
.set(PROJECTS.CREATED_AT, project.createdAt)
.execute()
}
fun create(project: Project) {
val record = PROJECTS.newRecord().from(project)
record.insert()
}
fun saveAll(projects: List<Project>) {
dsl.batchInsert(PROJECTS.newRecords(projects)).execute()
}
fun saveAll(projects: List<Project>) {
dsl.batchInsert(projects.map { project ->
dsl.newRecord(PROJECTS, project)
}).execute()
}
insertInto가 JPA와 다르게 확실히 편하다. saveAll도 간편하게 가능하다.
UDATE
fun update(project: Project) {
val result = dsl.update(PROJECTS)
.set(PROJECTS.NAME, project.name)
.set(PROJECTS.TYPE, project.type)
.set(PROJECTS.VERSION, project.version)
.set(PROJECTS.UPDATED_AT, LocalDateTime.now())
.where(PROJECTS.ID.eq(project.id).and(PROJECTS.CREATOR_ID.eq(project.creatorId)))
.execute()
}
fun update(project: Project) {
val record = PROJECTS.newRecord()
record.from(project)
val result = record.update()
}
fun moveTo(userId: String, targetParentId: String, ids: List<String>) {
dsl.update(PROJECTS)
.set(PROJECTS.PARENT_ID, targetParentId)
.where(PROJECTS.ID.`in`(ids).and(PROJECTS.CREATOR_ID.eq(userId)))
.execute()
}
개인적으로 JPA는 update가 불편하다고 생각한다. 명시적으로 save를 써야하고, setter만으로 처리되는 경우도 있다. 개인적으로는 너무 별로라 생각하고, 좀 괜찮은 방식인 DynamicUpdate는 entity에 붙는다.
JOOQ의 update는 누가봐도 update다. 컬럼별 업데이트도 진짜 쉽다.
DELETE
fun deleteByIds(ids: List<String>) {
dsl.deleteFrom(PROJECTS)
.where(PROJECTS.ID.`in`(ids))
.execute()
}
fun deleteById(id: String) {
dsl.deleteFrom(PROJECTS)
.where(PROJECTS.ID.eq(id))
.execute()
}
fun deleteOlderThan(dateTime: LocalDateTime) {
dsl.deleteFrom(PROJECTS)
.where(PROJECTS.CREATED_AT.lt(dateTime))
.execute()
}
CD는 JPA가 편하긴 한 것 같다.
마치며
개인적인 취향이지만 모든게 간소화된 JPA에 비해서 JOOQ의 명시적인 방식이 나한텐 좀 더 맞다. 그리고 DSL 형식이 상대적으로 덜한 것도 더 잘 맞았다.
다음 장은 뭘 써야 되는지 잘 모르겠지만, 트랜잭션 처리나 CTE 같은걸 알아보지 않을까 싶다.