개발/SPRING

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

애쿠 2025. 3. 23. 23:54

 

저번 글에서, 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 같은걸 알아보지 않을까 싶다.