Skip to content

Commit

Permalink
Example for Micronaut Data JPA
Browse files Browse the repository at this point in the history
  • Loading branch information
quandor committed Oct 18, 2023
1 parent 4fa7403 commit 25efbcc
Show file tree
Hide file tree
Showing 15 changed files with 246 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ scale projects.
** link:examples/caching[Caching]
** link:examples/http-server[HTTP-Server]
** link:examples/graphql[GraphQL]
** Micronaut-Data (tbd)
** link:examples/data-jpa[Micronaut-Data] using JPA
** link:examples/rabbitmq[RabbitMQ]
** Kafka (tbd)
** Security (tbd)
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,5 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:${kotlinVersion}")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:${kotlinVersion}")
testImplementation("io.mockk:mockk")
testImplementation("io.kotest:kotest-assertions-core-jvm")
}
5 changes: 5 additions & 0 deletions examples/data-jpa/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
= Micronaut: Data JDBC

Showcase demonstrating how to test JPA repositories.

This showcase uses https://github.com/micronaut-projects/micronaut-test-resources[Micronaut test resources]. Have a look at `build.gradle.kts` to see an easy and painless way to automatically provide a database during tests.
23 changes: 23 additions & 0 deletions examples/data-jpa/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
plugins {
id("cnt-micronaut.micronaut-conventions")
// this plugin automatically uses TestContainers to provide a postgres database for tests
id("io.micronaut.test-resources")
kotlin("plugin.jpa") version "1.8.22"
kotlin("plugin.noarg") version "1.8.22"
}

dependencies {
ksp("io.micronaut.data:micronaut-data-processor")
implementation("io.micronaut.data:micronaut-data-hibernate-jpa")
implementation("io.micronaut.flyway:micronaut-flyway")
implementation("io.micronaut.sql:micronaut-jdbc-hikari")
runtimeOnly("org.postgresql:postgresql")
}

application {
mainClass.set("example.micronaut.DataApplicationKt")
}

noArg {
annotation("example.data.jpa.model.NoArgConstructor")
}
15 changes: 15 additions & 0 deletions examples/data-jpa/src/main/kotlin/example/data/jpa/model/Book.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package example.data.jpa.model

import example.data.jpa.persistence.IsbnConverter
import example.data.jpa.persistence.TitleConverter
import jakarta.persistence.Convert
import jakarta.persistence.Embeddable

@NoArgConstructor
@Embeddable
data class Book(
@field:Convert (converter = IsbnConverter::class)
val isbn: Isbn,
@field:Convert (converter = TitleConverter::class)
val title: Title
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package example.data.jpa.model

private val pattern = Regex("([0-9]{3}(-)?)?[0-9]{10}")

data class Isbn(val value: String) {
init {
require(value matches pattern) { "ISBN [$value] must match pattern [$pattern]!" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package example.data.jpa.model

@Retention
@Target(AnnotationTarget.CLASS)
annotation class NoArgConstructor
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package example.data.jpa.model

data class Title(val value: String) {
init {
require(value.isNotBlank()) { "Titles must not be blank!" }
require(value.length <= 100) { "Titles longer than 100 characters are not allowed!" }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package example.data.jpa.persistence

import example.data.jpa.model.Book
import jakarta.persistence.Embedded
import jakarta.persistence.Entity
import jakarta.persistence.Id
import jakarta.persistence.Table
import jakarta.persistence.Version
import java.util.UUID

@Entity
@Table(name = "books")
class BookEntity(
@Id
val id: UUID,
@Embedded
var book: Book,
@Version
val version: Int = 0
) {

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as BookEntity

if (id != other.id) return false
if (book != other.book) return false
if (version != other.version) return false

return true
}

override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + book.hashCode()
result = 31 * result + version.hashCode()
return result
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package example.data.jpa.persistence

import example.data.jpa.model.Title
import io.micronaut.data.annotation.Query
import io.micronaut.data.annotation.Repository
import io.micronaut.data.jpa.repository.JpaRepository
import java.util.UUID

@Repository
interface BookEntityRepository : JpaRepository<BookEntity, UUID> {
@Query("SELECT b FROM BookEntity b WHERE b.book.title = :title")
fun findByTitle(title: Title): List<BookEntity>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package example.data.jpa.persistence

import example.data.jpa.model.Isbn
import example.data.jpa.model.Title
import jakarta.inject.Singleton
import jakarta.persistence.AttributeConverter
import jakarta.persistence.Converter

@Singleton
@Converter
class TitleConverter : AttributeConverter<Title, String> {
override fun convertToDatabaseColumn(attribute: Title?) = attribute?.value

override fun convertToEntityAttribute(dbData: String?) = dbData?.let(::Title)
}

@Singleton
@Converter
class IsbnConverter : AttributeConverter<Isbn, String> {
override fun convertToDatabaseColumn(attribute: Isbn?) = attribute?.value

override fun convertToEntityAttribute(dbData: String?) = dbData?.let(::Isbn)
}
6 changes: 6 additions & 0 deletions examples/data-jpa/src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
micronaut.application.name=cnt-micronaut.data-jpa
datasources.default.db-type=postgres
datasources.default.dialect=POSTGRES
jpa.default.properties.hibernate.hbm2ddl.auto=none
datasources.default.driver-class-name=org.postgresql.Driver
flyway.datasources.default.enabled=true
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE books
(
id uuid,
title text,
isbn varchar(13),
version bigint,
PRIMARY KEY (id)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package example.data.jpa.persistence

import example.data.jpa.model.Book
import example.data.jpa.model.Isbn
import example.data.jpa.model.Title
import io.kotest.assertions.throwables.shouldThrowExactly
import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder
import io.kotest.matchers.optional.shouldBePresent
import io.kotest.matchers.shouldBe
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import org.junit.jupiter.api.Disabled
import org.junit.jupiter.api.Test
import kotlin.random.Random.Default.nextInt
import java.util.UUID

@MicronautTest
class BookEntityRepositoryTest(
private val cut: BookEntityRepository
) {

@Test
fun `entity can be saved`() {
val entity = bookRecordEntity()

val savedEntity = cut.save(entity)

savedEntity.shouldBe(entity)
}

@Test
fun `entity version is increased with every change`() {
// Micronaut never returns a new Object after save, but applies all changes in place
// @Version functionality is only triggered after a flush hence we call saveAndFlush
val entity = bookRecordEntity()

val savedEntity1 = cut.saveAndFlush(entity)
savedEntity1.version.shouldBe(0)

val savedEntity2 = cut.saveAndFlush(savedEntity1.changeTitle())
savedEntity2.version.shouldBe(1)

val savedEntity3 = cut.saveAndFlush(savedEntity2)
savedEntity3.version.shouldBe(1)

val savedEntity4 = cut.saveAndFlush(savedEntity3.changeTitle())
savedEntity4.version.shouldBe(2)
}

@Test
@Disabled
// TODO Any idea whether how to test this? It is not easy, as Micronaut only has a single object
// with the same Identifier.
fun `entity can not be saved in lower than current version`() {
val entity = bookRecordEntity()
val savedEntity1 = cut.save(entity)
cut.save(savedEntity1.changeTitle())

shouldThrowExactly<IllegalArgumentException> { cut.save(savedEntity1) }
}

@Test
fun `entity can be found by id`() {
val savedEntity = cut.save(bookRecordEntity())

val foundEntity = cut.findById(savedEntity.id)

foundEntity.shouldBePresent().shouldBe(savedEntity)
}

@Test
fun `entity can be found by title`() {
val e1 = cut.save(bookRecordEntity("Clean Code"))
cut.save(bookRecordEntity("Clean Architecture"))
val e3 = cut.save(bookRecordEntity("Clean Code"))

val foundEntities = cut.findByTitle(Title("Clean Code"))

foundEntities.shouldContainExactlyInAnyOrder(e1, e3)
}

private fun bookRecordEntity(title: String = "Clean Code") =
BookEntity(UUID.randomUUID(), Book(Isbn("9780123456789"), Title(title)))

private fun BookEntity.changeTitle(): BookEntity =
apply { book = book.copy(title = Title("Change Title #${nextInt(1_000)}")) }
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ include("examples:http-server")
include("examples:caching")
include("examples:graphql")
include("examples:rabbitmq")
include("examples:data-jpa")

0 comments on commit 25efbcc

Please sign in to comment.