-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
1 parent
76316f3
commit fa6f2a4
Showing
5 changed files
with
269 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,5 +21,6 @@ include( | |
"speed-client", | ||
"spenn-simulering-client", | ||
"spedisjon-client", | ||
"jackson" | ||
"jackson", | ||
"sql-dsl" | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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")) | ||
} |
56 changes: 56 additions & 0 deletions
56
sql-dsl/src/main/kotlin/com/github/navikt/tbd_libs/sql_dsl/Query.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
140 changes: 140 additions & 0 deletions
140
sql-dsl/src/test/kotlin/com/github/navikt/tbd_libs/sql_dsl/QueryTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |