콘텐츠로 이동
Bluetape4k 문서1.11

ResultSet 읽기와 mapping

최신 안정판 Bluetape4k 1.11.0 릴리스 기준

JDBC의 getInt, getLong, getBoolean 같은 primitive getter는 SQL NULL에서도 0이나 false를 반환할 수 있습니다. NULL 여부는 바로 뒤의 wasNull()로 확인해야 합니다. getIntOrNull, getLongOrNull 같은 확장은 이 두 호출을 묶어 Kotlin nullable 값으로 반환합니다.

data class AccountRow(
val id: Long,
val retryCount: Int?,
val nickname: String?,
)
val accounts = dataSource.executeQuery(
"SELECT id, retry_count, nickname FROM accounts",
) { rs ->
rs.toList { row ->
AccountRow(
id = row.getLong("id"),
retryCount = row.getIntOrNull("retry_count"),
nickname = row.getString("nickname"),
)
}
}

column index는 JDBC 규칙대로 1부터 시작합니다. label 접근은 SQL alias를 존중하므로 projection 이름을 안정적으로 유지할 때 유용합니다.

결과 개수 계약을 코드에 드러낸다

섹션 제목: “결과 개수 계약을 코드에 드러낸다”
예상 결과API결과가 없을 때두 행 이상일 때
0~1행mapFirstnull첫 행만 반환
정확히 1행mapSingleNoSuchElementExceptionIllegalStateException
1행 이상 중 첫 행mapFirstOrThrowNoSuchElementException첫 행만 반환
여러 행toList, toSet, toMap, groupBy빈 collection모두 소비

mapFirst는 query가 실제로 unique하다는 사실을 검증하지 않습니다. unique 결과가 업무 계약이면 SQL 제약과 함께 mapSingle로 예상 cardinality를 확인합니다.

대부분의 mapping 함수는 ResultSet.next()를 호출하면서 현재 위치부터 끝까지 전진합니다. 같은 ResultSetcount, any, toList를 차례로 호출하면 뒤 함수는 앞 함수가 남긴 위치부터 시작합니다. 한 cursor는 한 번의 변환에만 사용하고, 다른 형태가 필요하면 변환된 collection에서 처리합니다.

isEmptyByMovingCursorisNotEmptyByMovingCursor는 이름 그대로 cursor를 한 행 이동합니다. non-empty 결과라면 cursor가 첫 행에 놓이므로 바로 그 행을 읽어야 합니다. 이어서 toList를 호출하면 두 번째 행부터 담깁니다. 예전 isEmptyisNotEmpty 이름은 이 부작용이 드러나지 않아 deprecated 되었습니다.

dataSource.executeQuery("SELECT id FROM accounts ORDER BY id") { rs ->
if (rs.isNotEmptyByMovingCursor()) {
val firstId = rs.getLong("id") // 현재 첫 행을 먼저 소비
val remainingIds = rs.toList { it.getLong("id") }
listOf(firstId) + remainingIds
} else {
emptyList()
}
}

forward-only driver에서는 moveToPrevious()가 실패할 수 있으며 이 함수는 그때 false를 반환합니다. cursor를 되돌리는 것을 보편적인 복구 방법으로 삼지 않습니다.

sequencemapAsSequenceResultSet cursor를 지연 소비합니다. sequence 자체가 데이터를 복사해 두는 것은 아닙니다. 아래처럼 mapper block 안에서 terminal operation까지 끝내야 합니다.

val ids: List<Long> = dataSource.executeQuery(
"SELECT id FROM accounts ORDER BY id",
) { rs ->
rs.sequence { row -> row.getLong("id") }
.take(100)
.toList()
}

mapper 밖으로 sequence를 반환하면 executeQuery가 statement와 ResultSet을 먼저 닫습니다. 나중에 sequence를 순회할 때 이미 닫힌 cursor를 읽게 됩니다.

여러 타입을 반복해서 읽는 projection은 extractResultSetGetColumnTokens를 사용할 수 있습니다.

val rows = dataSource.executeQuery(
"SELECT id, display_name FROM accounts",
) { rs ->
rs.extract {
AccountRow(
id = long["id"]!!,
retryCount = null,
nickname = string["display_name"],
)
}
}

!!는 SQL과 schema가 NULL을 금지한다는 계약이 있을 때만 사용합니다. nullable column이라면 도메인 타입도 nullable로 유지하거나 mapping 경계에서 명시적인 기본값·오류 정책을 둡니다.

조회 결과를 값으로 옮기는 규칙을 정했다면 트랜잭션과 상태 복원에서 여러 statement의 성공·실패 경계를 묶습니다.