Skip to content

Commit

Permalink
add examples for new JdbcClient
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Nov 23, 2023
1 parent d677a2c commit 10607bf
Show file tree
Hide file tree
Showing 4 changed files with 267 additions and 12 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package example.spring.boot.jdbc.persistence

import example.spring.boot.jdbc.business.Book
import example.spring.boot.jdbc.business.BookRecord
import org.slf4j.LoggerFactory.getLogger
import org.springframework.dao.DuplicateKeyException
import org.springframework.dao.IncorrectResultSizeDataAccessException
import org.springframework.jdbc.core.simple.JdbcClient
import org.springframework.stereotype.Repository
import org.springframework.util.IdGenerator
import java.util.UUID
import kotlin.jvm.optionals.getOrNull

@Repository
class ClientBasedBookRecordRepository(
private val client: JdbcClient,
private val idGenerator: IdGenerator
) {

private val log = getLogger(javaClass)

fun create(book: Book): BookRecord {
val id = idGenerator.generateId()
return try {
client.sql("INSERT INTO book_records (id, title, isbn) VALUES (:id, :title, :isbn)")
.param("id", id)
.param("title", book.title)
.param("isbn", book.isbn)
.update()
BookRecord(id, book)
} catch (e: DuplicateKeyException) {
log.warn("ID collision occurred for ID [{}] - retrying with new ID", id)
create(book)
}
}

fun update(bookRecord: BookRecord): Boolean =
client.sql("UPDATE book_records SET title = :title, isbn = :isbn WHERE id = :id")
.param("id", bookRecord.id)
.param("title", bookRecord.book.title)
.param("isbn", bookRecord.book.isbn)
.update() != 0

fun findBy(id: UUID): BookRecord? =
try {
client.sql("SELECT * FROM book_records WHERE id = :id")
.param("id", id)
.query { rs, _ ->
val title = rs.getString("title")!!
val isbn = rs.getString("isbn")
BookRecord(id, Book(title, isbn))
}
.optional()
.getOrNull()
} catch (e: IncorrectResultSizeDataAccessException) {
null
}

fun deleteBy(id: UUID): Boolean =
client.sql("DELETE FROM book_records WHERE id = :id")
.param("id", id)
.update() != 0

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import org.springframework.util.IdGenerator
import java.util.UUID

@Repository
class BookRecordRepository(
class TemplateBasedBookRecordRepository(
private val jdbcTemplate: NamedParameterJdbcTemplate,
private val idGenerator: IdGenerator
) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
package example.spring.boot.jdbc.persistence

import com.ninjasquad.springmockk.MockkBean
import example.spring.boot.jdbc.business.Book
import example.spring.boot.jdbc.business.BookRecord
import example.spring.boot.jdbc.utils.InitializeWithContainerizedPostgreSQL
import io.mockk.clearMocks
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.h2.jdbcx.JdbcDataSource
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest
import org.springframework.context.annotation.Import
import org.springframework.jdbc.core.simple.JdbcClient
import org.springframework.test.context.ActiveProfiles
import org.springframework.util.IdGenerator
import java.util.UUID.randomUUID

internal class ClientBasedBookRecordRepositoryTests {

/**
* Fastest boostrap, but only simulates PostgreSQL behaviour.
*/
@Nested
inner class AsUnitTest : ClientBasedBookRecordRepositoryContract() {

val dataSource = JdbcDataSource()
.apply { setUrl("jdbc:h2:mem:${randomUUID()};MODE=PostgreSQL;DB_CLOSE_DELAY=-1") }
.apply { user = "sa"; password = "sa" }
.also {
Flyway.configure()
.dataSource(it)
.locations("classpath:db/migration")
.load()
.migrate()
}

override val idGenerator: IdGenerator = mockk()
override val cut = ClientBasedBookRecordRepository(JdbcClient.create(dataSource), idGenerator)

}

/**
* Much faster boostrap, but only simulates PostgreSQL behaviour.
*/
@Nested
@JdbcTest
@ActiveProfiles("test", "in-memory")
@MockkBean(IdGenerator::class)
@Import(ClientBasedBookRecordRepository::class)
inner class AsTechnologyIntegrationTestWithH2InMemoryDatabase(
@Autowired override val idGenerator: IdGenerator,
@Autowired override val cut: ClientBasedBookRecordRepository
) : ClientBasedBookRecordRepositoryContract()

/**
* Takes longer to boostrap, but also provides real PostgreSQL behaviour.
*
* This actually discovered that with PostgreSQL and native JDBC, a UUID type
* column cannot be queries using a String representation of a UUID.
*/
@Nested
@JdbcTest
@ActiveProfiles("test", "docker")
@MockkBean(IdGenerator::class)
@Import(ClientBasedBookRecordRepository::class)
@InitializeWithContainerizedPostgreSQL
inner class AsTechnologyIntegrationTestWithDockerizedDatabase(
@Autowired override val idGenerator: IdGenerator,
@Autowired override val cut: ClientBasedBookRecordRepository
) : ClientBasedBookRecordRepositoryContract()

abstract class ClientBasedBookRecordRepositoryContract {

protected abstract val idGenerator: IdGenerator
protected abstract val cut: ClientBasedBookRecordRepository

val cleanCode = Book("Clean Code", "9780132350884")
val cleanArchitecture = Book("Clean Architecture", "9780134494166")

val id1 = randomUUID()
val id2 = randomUUID()

@BeforeEach
fun resetMocks() {
clearMocks(idGenerator)
}

@Nested
inner class Creating {

@Test
fun `creating a book returns a book record`() {
every { idGenerator.generateId() } returns id1
val bookRecord = cut.create(cleanCode)
assertThat(bookRecord).isEqualTo(BookRecord(id1, cleanCode))
}

@Test
fun `duplicated keys during creation are handled`() {
every { idGenerator.generateId() } returnsMany listOf(id1, id1, id2)

val bookRecord1 = cut.create(cleanArchitecture)
val bookRecord2 = cut.create(cleanArchitecture)

assertThat(bookRecord1.id).isEqualTo(id1)
assertThat(bookRecord2.id).isEqualTo(id2)

verify(exactly = 3) { idGenerator.generateId() } // there was a retry
}

}

@Nested
inner class Getting {

@Test
fun `existing book records can be found by id`() {
every { idGenerator.generateId() } returns id1

val bookRecord = cut.create(cleanCode)
val foundBookRecord = cut.findBy(bookRecord.id)

assertThat(foundBookRecord).isEqualTo(bookRecord)
}

@Test
fun `non existing book records are returned as null when trying to find them by id`() {
assertThat(cut.findBy(id2)).isNull()
}

}

@Nested
inner class Updating {

@Test
fun `updating an existing book record changes all its data except the id`() {
every { idGenerator.generateId() } returns id1

val created = cut.create(cleanCode)
assertThat(cut.findBy(id1)).isEqualTo(created)

val changed = created.copy(book = cleanArchitecture)
val wasUpdated = cut.update(changed)
assertThat(wasUpdated).isTrue()

assertThat(cut.findBy(id1)).isEqualTo(changed)
}

@Test
fun `updating non existing book returns false`() {
val bookRecord = BookRecord(id2, cleanCode)
val wasUpdated = cut.update(bookRecord)
assertThat(wasUpdated).isFalse()
}

}

@Nested
inner class Deleting {

@Test
fun `existing book records can be deleted by id`() {
every { idGenerator.generateId() } returns id1

val bookRecord = cut.create(cleanCode)
assertThat(cut.findBy(id1)).isEqualTo(bookRecord)

val wasDeleted = cut.deleteBy(id1)
assertThat(wasDeleted).isTrue()
assertThat(cut.findBy(bookRecord.id)).isNull()
}

@Test
fun `deleting non existing book record throws exception`() {
val wasDeleted = cut.deleteBy(id2)
assertThat(wasDeleted).isFalse()
}

}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ import org.springframework.test.context.ActiveProfiles
import org.springframework.util.IdGenerator
import java.util.UUID.randomUUID

internal class BookRecordRepositoryTests {
internal class TemplateBasedBookRecordRepositoryTests {

/**
* Fastest boostrap, but only simulates PostgreSQL behaviour.
*/
@Nested
inner class AsUnitTest : BookRecordRepositoryContract() {
inner class AsUnitTest : TemplateBasedBookRecordRepositoryContract() {

val dataSource = JdbcDataSource()
.apply { setUrl("jdbc:h2:mem:${randomUUID()};MODE=PostgreSQL;DB_CLOSE_DELAY=-1") }
Expand All @@ -44,7 +44,7 @@ internal class BookRecordRepositoryTests {
val jdbcTempalte = NamedParameterJdbcTemplate(dataSource)

override val idGenerator: IdGenerator = mockk()
override val cut = BookRecordRepository(jdbcTempalte, idGenerator)
override val cut = TemplateBasedBookRecordRepository(jdbcTempalte, idGenerator)

}

Expand All @@ -55,11 +55,11 @@ internal class BookRecordRepositoryTests {
@JdbcTest
@ActiveProfiles("test", "in-memory")
@MockkBean(IdGenerator::class)
@Import(BookRecordRepository::class)
@Import(TemplateBasedBookRecordRepository::class)
inner class AsTechnologyIntegrationTestWithH2InMemoryDatabase(
@Autowired override val idGenerator: IdGenerator,
@Autowired override val cut: BookRecordRepository
) : BookRecordRepositoryContract()
@Autowired override val cut: TemplateBasedBookRecordRepository
) : TemplateBasedBookRecordRepositoryContract()

/**
* Takes longer to boostrap, but also provides real PostgreSQL behaviour.
Expand All @@ -71,17 +71,17 @@ internal class BookRecordRepositoryTests {
@JdbcTest
@ActiveProfiles("test", "docker")
@MockkBean(IdGenerator::class)
@Import(BookRecordRepository::class)
@Import(TemplateBasedBookRecordRepository::class)
@InitializeWithContainerizedPostgreSQL
inner class AsTechnologyIntegrationTestWithDockerizedDatabase(
@Autowired override val idGenerator: IdGenerator,
@Autowired override val cut: BookRecordRepository
) : BookRecordRepositoryContract()
@Autowired override val cut: TemplateBasedBookRecordRepository
) : TemplateBasedBookRecordRepositoryContract()

abstract class BookRecordRepositoryContract {
abstract class TemplateBasedBookRecordRepositoryContract {

protected abstract val idGenerator: IdGenerator
protected abstract val cut: BookRecordRepository
protected abstract val cut: TemplateBasedBookRecordRepository

val cleanCode = Book("Clean Code", "9780132350884")
val cleanArchitecture = Book("Clean Architecture", "9780134494166")
Expand Down

0 comments on commit 10607bf

Please sign in to comment.