Skip to content

Commit

Permalink
et lite bibliotek for query-hjelp
Browse files Browse the repository at this point in the history
i første omgang bare hjelp til å starte en transaksjon og å hente ut data fra ResultSet.

ser an litt behovet for å tilby noe for Prepared Statement, f.eks. å binde parametre til spørringen, og sånne ting.
  • Loading branch information
davidsteinsland committed Feb 10, 2025
1 parent 76316f3 commit fa6f2a4
Show file tree
Hide file tree
Showing 5 changed files with 269 additions and 1 deletion.
3 changes: 2 additions & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ include(
"speed-client",
"spenn-simulering-client",
"spedisjon-client",
"jackson"
"jackson",
"sql-dsl"
)
58 changes: 58 additions & 0 deletions sql-dsl/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
Query
=====

Litt hjelp for å effektivisere bruken av `java.sql`, men ikke så mye mer enn det!

```kotlin
fun Connection.createName(name: String?) =
prepareStatement("insert into name(name) values (?) returning id").use { stmt ->
if (name == null) stmt.setObject(1, null) else stmt.setString(1, name)
stmt.executeQuery().single { row -> row.getLong(1) }
}

fun Connection.findName(id: Long) =
prepareStatement("select name from name where id = ? limit 1").use { stmt ->
stmt.setLong(1, id)
stmt.executeQuery().singleOrNull { row -> row.getString("name") }
}

fun main() = dataSource.connection.use { connection ->
@Language("PostgreSQL")
val sql = """create table name (
id bigint primary key generated always as identity,
name text,
created timestamptz not null default now()
)"""
connection.createStatement().execute(sql)

val (hansId, nullId) = connection.transaction {
val hansId = connection.createName("hans")
val nullId = connection.createName(null)
Pair(hansId, nullId)
}

assertEquals("hans", connection.findName(hansId))
assertEquals(null, connection.findName(nullId))
try {
connection.findName(1000)
} catch (err: NoSuchElementException) {
// raden finnes ikke
}

val mapName = { rs: ResultSet -> rs.getString("name") }
val namesWithNull = connection.prepareStatement("select name from name").use {
// map godtar null-rader
it.executeQuery().map(mapName)
}

assertEquals(listOf("hans", null), namesWithNull)

val namesWithoutNull = connection.prepareStatement("select name from name").use {
// mapNotNull godtar ikke null-rader
it.executeQuery().mapNotNull(mapName)
}

assertEquals(listOf("hans"), namesWithoutNull)
}
```

13 changes: 13 additions & 0 deletions sql-dsl/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
val postgresqlVersion = "42.7.4"
val hikariCPVersion = "6.1.0"

dependencies {
// konsumenter av biblioteket må selv vurdere hvilken hikari de vil ha
// (implementation 'lekker' ikke ut på compile-classpath til konsumentene
testImplementation("com.zaxxer:HikariCP:$hikariCPVersion")
testImplementation("org.postgresql:postgresql:$postgresqlVersion") {
exclude(group = "junit", module = "junit")
exclude(group = "org.slf4j", module = "slf4j-api")
}
testImplementation(project(":postgres-testdatabaser"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.github.navikt.tbd_libs.sql_dsl

import java.sql.Connection
import java.sql.ResultSet
import javax.sql.DataSource

fun <R> DataSource.connection(block: Connection.() -> R): R {
return connection.use(block)
}

// krever minst én rad og at mapping-funksjonen ikke returnerer null
fun <R> ResultSet.single(map: (ResultSet) -> R?): R {
return checkNotNull(singleOrNull(map)) { "forventet ikke er null-verdi" }
}

// krever én rad, men mapping-funksjonen kan returnere null
fun <R> ResultSet.singleOrNull(map: (ResultSet) -> R?): R? {
return this.map(map).single()
}

// returnerer null hvis resultatet er tomt eller mapping-funksjonen returnerer null
fun <R> ResultSet.firstOrNull(map: (ResultSet) -> R?): R? {
return this.map(map).firstOrNull()
}

// siden flere av ResultSet-funksjonene returnerer potensielt null
// så føles det mer riktig å anta at map-funksjonen kan gi en nullable R.
// f.eks. vil ResultSet.getString() returnere `null` hvis kolonnen er lagret som `null` i databasen.
// i kotlin vil typen bli seende som `String!`, som kan godtas både som `String` og `String?` i kotlin.
// Det kan dessuten være legitimt bruksområde å hente ut rader, men bevare `null`-verdien. derfor foretas det ingen filtrering her.
// bruk `mapNotNull()` for å fjerne null-rader / gjøre listen not-null
fun <R> ResultSet.map(map: (ResultSet) -> R?): List<R?> {
return buildList {
while (next()) {
add(map(this@map))
}
}
}

fun <R> ResultSet.mapNotNull(map: (ResultSet) -> R?): List<R> = map(map).filterNotNull()

fun <R> Connection.transaction(block: Connection.() -> R): R {
return try {
autoCommit = false
block().also { commit() }
} catch (err: Exception) {
try {
rollback()
} catch (suppressed: Exception) {
err.addSuppressed(suppressed)
}
throw err
} finally {
autoCommit = true
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package com.github.navikt.tbd_libs.sql_dsl

import com.github.navikt.tbd_libs.test_support.DatabaseContainers
import java.sql.Connection
import java.sql.ResultSet
import javax.sql.DataSource
import org.intellij.lang.annotations.Language
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertThrows

class QueryTest {

@Test
fun `single returnerer én ikke-null rad, kaster exception hvis ikke`() = setupTest { connection ->
val hansId = connection.createName("hans")
val nullId = connection.createName(null)

val mapName = { rs: ResultSet -> rs.getString("name") }
assertEquals("hans", connection.name(hansId).single(mapName))
assertThrows<IllegalStateException> { assertEquals(null, connection.name(nullId).single(mapName)) }
assertThrows<NoSuchElementException> { assertEquals(null, connection.name(1000).single(mapName)) }
}

@Test
fun `singleOrNull returnerer én potensielt null-rad, kaster exception ved tomt resultat`() = setupTest { connection ->
val hansId = connection.createName("hans")
val nullId = connection.createName(null)

val mapName = { rs: ResultSet -> rs.getString("name") }
assertEquals("hans", connection.name(hansId).singleOrNull(mapName))
assertEquals(null, connection.name(nullId).singleOrNull(mapName))
assertThrows<NoSuchElementException> { connection.name(1000).singleOrNull(mapName) }
}

@Test
fun `firstOrNull returnerer potensiell null-rad hvis den finnes, null ellers`() = setupTest { connection ->
val hansId = connection.createName("hans")
val nullId = connection.createName(null)

val mapName = { rs: ResultSet -> rs.getString("name") }
assertEquals("hans", connection.name(hansId).firstOrNull(mapName))
assertEquals(null, connection.name(nullId).firstOrNull(mapName))
assertEquals(null, connection.name(1000).firstOrNull(mapName))
}

@Test
fun `map omformer hver rad, godtar at resultatet er null`() = setupTest { connection ->
connection.createName("hans")
connection.createName(null)

val mapName = { rs: ResultSet -> rs.getString("name") }
val names = connection.prepareStatement("select name from name").use {
it.executeQuery().map(mapName)
}

assertEquals(listOf("hans", null), names)
}

@Test
fun `mapNoptNull omformer hver ikke-nulll rad`() = setupTest { connection ->
connection.createName("hans")
connection.createName(null)

val mapName = { rs: ResultSet -> rs.getString("name") }
val names = connection.prepareStatement("select name from name").use {
it.executeQuery().mapNotNull(mapName)
}

assertEquals(listOf("hans"), names)
}

@Test
fun `transaction ruller tilbake ved feil`() = setupTest { connection ->
assertThrows<IllegalStateException> {
connection.transaction {
connection.createName("hans")
error("something went wrong")
}
}
assertEquals(emptyList<Any>(), connection.names())

assertTrue(connection.autoCommit) { "transaction må sette autoCommit tilbake" }
connection.createName("hans")
assertEquals(listOf("hans"), connection.names())
}

@Test
fun `transaction committer hvis alt er ok`() = setupTest { connection ->
connection.transaction { connection.createName("hans") }
assertEquals(listOf("hans"), connection.names())
}

private fun Connection.names(): List<String?> {
val mapName = { rs: ResultSet -> rs.getString("name") }
return prepareStatement("select name from name").use { it.executeQuery().map(mapName) }
}

private fun Connection.name(id: Long) =
prepareStatement("select name from name where id = ? limit 1").let { stmt ->
stmt.setLong(1, id)
stmt.executeQuery()
}

private fun Connection.createName(name: String?) =
prepareStatement("insert into name(name) values (?) returning id").use { stmt ->
if (name == null) stmt.setObject(1, null) else stmt.setString(1, name)
stmt.executeQuery().single { row -> row.getLong(1) }
}

private fun Connection.createTestTable() {
@Language("PostgreSQL")
val sql = """create table name (
id bigint primary key generated always as identity,
name text,
created timestamptz not null default now()
)"""
createStatement().execute(sql)
}

private fun setupTest(testblokk: (Connection) -> Unit) {
dbTest { db ->
db.connection.use { connection ->
connection.createTestTable()
testblokk(connection)
}
}
}
}

private val databaseContainer = DatabaseContainers.container("sql-dsl")
fun dbTest(testblokk: (DataSource) -> Unit) {
val testDataSource = databaseContainer.nyTilkobling()
try {
testblokk(testDataSource.ds)
} finally {
databaseContainer.droppTilkobling(testDataSource)
}
}

0 comments on commit fa6f2a4

Please sign in to comment.