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 를 만들어보고자 한다.
우선적으로 가장 시급한 요구사항을 나열해보자.
students[i].name?.let { ps.setString(1, it) } ?: ps.setNull(1, Types.NULL)
처럼 매번 let 을 통해 구분해주는 것은 매우 불편하다.우리가 만들 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))
글 잘 봤습니다.