본문 바로가기
코틀린

ResultSet 에서 List<T> 로 쉽게 변환하기

by RWriter 2021. 8. 5.
반응형

예제소스는 깃허브에 있습니다.

 

ResultSet 을 직접 사용할 일은 거의 없을 것이다. ORM 이 잘 처리해 주고 있기 때문..

 

하지만 요새는 JPA 말고 코틀린 진영에서 lightweight but powerful 한 ORM 프레임워크도 종종 사용이 되는 것 같은데 

 

exposed 라는 것이다.

https://github.com/JetBrains/Exposed

 

GitHub - JetBrains/Exposed: Kotlin SQL Framework

Kotlin SQL Framework. Contribute to JetBrains/Exposed development by creating an account on GitHub.

github.com

 

spring starter 도 있어서 @Transactional 어노테이션과 혼용해서 사용도 가능하고, 무척 괜찮아 보인다.

 

exposed는 트랜잭션을 AOP 기반 어노테이션 방식이 아닌 functional 하게 관리할 수 있고, native query 도 쉽게 잘 쓸 수 있다.

 

여기서 ResultSet 을 얻어올 수 있는데 iterable을 돌리는 방식보다 재귀 & 리플렉션을 사용해서 List로 변환할 수 있을 것 같아 짜봤다.

fun <T> recursiveExtract(resultSet: ResultSet, clazz: Class<T>): List<T> {
   // tailrec sub 함수를 사용하기 때문에 스택 낭비가 없다.
    tailrec fun recursiveExtract_(resultSet: ResultSet, list: LinkedList<T>): List<T> {
        return if (resultSet.next()) {
            list.add(resultSet.mapTo(clazz))
            recursiveExtract_(resultSet, list)
        } else {
            list
        }
    }
    return recursiveExtract_(resultSet, LinkedList())
}

fun <T> ResultSet.mapTo(clazz: Class<T>): T {
    val constructor = clazz.getDeclaredConstructor(*clazz.declaredFields.map { it.type }.toTypedArray())
    val dataList = clazz.declaredFields.map {
        val nameField = clazz.getDeclaredField(it.name)
        nameField.isAccessible = true
        this.getObject(it.name, nameField.type)
    }
    return constructor.newInstance(*dataList.toTypedArray())
}

 

코틀린이니 refied 를 써서 Class<T> 의 전달을 없앨 수도 있다.

inline fun <reified T> ResultSet.toList() = recursiveExtract(this, T::class.java)

 

테스트를 돌려보면

data class CityDto(val id: Int, val name: String)

class EntityTest {
    @Test
    fun test1() {
        Database.connect(dataSource)

        transaction {
            // print sql to std-out
            addLogger(
                StdOutSqlLogger,
//                Slf4jSqlDebugLogger
            )
            SchemaUtils.create(Cities)
            // insert new city. SQL: INSERT INTO Cities (name) VALUES ('St. Petersburg')
            val stPeteId = Cities.insert {
                it[name] = "St. Petersburg"
            } get Cities.id

            val cityDtoList_with_no_refied = exec("SELECT * FROM Cities") { rs -> recursiveExtract(rs, CityDto::class.java) } ?: listOf()
            
            // reified 가 인식되려면 변수에 타입을 명시적으로 지정해야 한다.
            val cityDtoList_withrefied: List<CityDto> = exec("SELECT * FROM Cities") { rs -> rs.toList() } ?: listOf()

            println(cityDtoList_with_no_refied)
            println(cityDtoList_withrefied)
        }
    }
}

 

쿼리 로깅과 CityDto 의 결과가 잘 나왔다. 

 

 

native 쿼리 말고도 exposed 에는 쉽고 다양하게 dsl query 도 만들 수 있고, 

JPA 에서는 하기 어려운 batch insert, batch update 등의 기능도 사용할 수 있다고 하니

 

다음 신규 프로젝트가 있다면, 충분히 고려해 볼 만한 기술인 것 같다.

반응형

댓글