JdbcTemplate의 raw string sql을 효과적으로 관리해보자

서민정·2023년 7월 23일
0

JdbcTemplate을 사용하게 되면 대부분 raw string으로 쿼리를 작성한다. 예를 들자면 다음과 같다. (여기서는 MySQL의 upsert 쿼리를 다룰 것이다.)

val sql = """
insert into students(id, name, age) values (?, ?, ?) 
on duplicate key update
name = ?, age = ?
""".trimIndent()

jdbcTemplate.batchUpdate(
    sql,
    object : BatchPreparedStatementSetter {
        override fun setValues(ps: PreparedStatement, i: Int) {
            ps.setLong(1, students[i].id)
            ps.setString(2, students[i].name)
            ps.setLong(3, students[i].age)
            ps.setString(4, students[i].name)
            ps.setLong(5, students[i].age)
    }
    override fun getBatchSize(): Int {
                    return aggregates.size
            }
    }
)

만약 students 테이블의 컬럼이 추가된다면, raw string으로 작성한 sql 에 ? 가 추가될 것이고, 그에 맞춰 ps.setSomething() 하는 부분도 추가되어야하며, 앞에 setSomething() 의 첫번째 인자인 인덱스도 일일이 세어서 늘려줘야한다.

만약 중간에 추가된다면? 컬럼의 수가 10개가 넘어가는 aggregate 테이블이라면..? 😢

그래서 Upsert 를 도와주는 UpsertManipulator 를 만들어보고자 한다.

UpsertManipulator

우선적으로 가장 시급한 요구사항을 나열해보자.

  • 인간지능을 이용한 인덱스 증가
    * 필드가 추가될 때마다 인덱스를 일일이 더해주는 것은 지옥이다.
  • Types.NULL
    * Nullable한 값에는 가져온 데이터가 null인 경우 Types.NULL 을 심어줘야하는데, students[i].name?.let { ps.setString(1, it) } ?: ps.setNull(1, Types.NULL) 처럼 매번 let 을 통해 구분해주는 것은 매우 불편하다.
  • 불필요한 반복
    * Upsert 쿼리의 경우, insert into 구문에 사용되는 값과 뒷부분의 update 구문에 사용되는 값은 결국 동일한데 raw string의 특성 상 뒷부분의 값을 다시 ps.setSomething() 해 주어야한다.

우리가 만들 UpsertManipulator 는 먼저 각 FieldValue를 저장할 수 있는 data class를 갖는다. 예를 들자면 다음과 같다.

sealed interface FieldDescription<E, T> {
    val fieldName: String
    val getter: (entity: E) -> T?

    fun valueExpression(): String = "?"

    // return next idx
    fun manipulate(ps: PreparedStatement, entity: E, idx: Int): Int
}
data class LongField<E>(
    override val fieldName: String,
    override val getter: (entity: E) -> Long?
) : FieldDescription<E, Long> {
    override fun manipulate(ps: PreparedStatement, entity: E, idx: Int): Int {
        getter(entity)?.let { ps.setLong(idx, it) } ?: ps.setNull(idx, Types.NULL)

        return idx + 1
    }
}

밖에서 ps.set{Field} 해주던 부분을, 각 타입별로 나누어서 데이터 클래스로 만들었다. 그리고 요구사항 2번 또한 내부에서 null인 경우 처리하도록 넣어두었고, 1번의 경우도 idx를 외부로부터 주입받아서 manipulate 후 1 증가된 인덱스를 반환해 다음 값에서 사용할 수 있도록 구현했다.
그리고 UpsertManipulator 는 우리가 작성하는 쿼리의 반복되는 패턴을 이용해 쿼리를 stringBuilder로 만들어 낼 수 있도록 구현했다.

data class UpsertManipulator<E>(
    val id: FieldDescription<E, *>,
    val tableName: String,
    val fields: List<FieldDescription<E, *>>
) {
    fun makeSql(): String {
        val stringBuilder = StringBuilder()
        stringBuilder.append("insert into $tableName")

        stringBuilder.append("(")
        stringBuilder.append(id.fieldName)
        fields.forEach {
            stringBuilder.append(", ${it.fieldName}")
        }
        stringBuilder.append(")")

        stringBuilder.append("values ")

        stringBuilder.append("(")
        stringBuilder.append(id.valueExpression())
        fields.forEach {
            stringBuilder.append(", ${it.valueExpression()}")
        }
        stringBuilder.append(")")

        stringBuilder.append("on duplicate key update ")
        fields.forEachIndexed { index, fieldDescription ->
            if (index != 0) stringBuilder.append(", ")
            stringBuilder.append("${fieldDescription.fieldName} = ${fieldDescription.valueExpression()}")
        }

        return stringBuilder.toString()
    }

    fun manipulate(entities: List<E>): BatchPreparedStatementSetter {
        return object : BatchPreparedStatementSetter {
            override fun setValues(ps: PreparedStatement, i: Int) {
                val entity = entities[i]
                var idx = 1

                // insert manipulate
                idx = id.manipulate(ps, entity, idx)
                fields.forEach {
                    idx = it.manipulate(ps, entity, idx)
                }

                // update manipulate
                fields.forEach {
                    idx = it.manipulate(ps, entity, idx)
                }
            }

            override fun getBatchSize(): Int {
                return entities.size
            }
        }
    }
}

외부에서는 아래처럼 호출하여 사용할 수 있다.

val manipulator = UpsertManipulator(
            id = LongField("id", Student::id),
            tableName = "students",
            fields = listOf(
                StringField("name", Student::name),
                IntField("age", Student::age),
            )
     )

jdbcTemplate.batchUpdate(manipulator.makeSql(), manipulator.manipulate(aggregates))
profile
Server Engineer

1개의 댓글

comment-user-thumbnail
2023년 7월 23일

글 잘 봤습니다.

답글 달기