Skip to content

Commit

Permalink
Merge pull request #171 from line/feature/jpa-reactive-3.0
Browse files Browse the repository at this point in the history
feat: support JPA 3.0 Reactive
  • Loading branch information
cj848 authored Jan 19, 2023
2 parents dad7e59 + 3c6b650 commit afcdcdb
Show file tree
Hide file tree
Showing 140 changed files with 12,291 additions and 19 deletions.
7 changes: 3 additions & 4 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ repositories {
dependencies {
}

tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xjvm-default=all")
jvmTarget = "11"
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(17))
}
}
6 changes: 6 additions & 0 deletions buildSrc/src/main/kotlin/Modules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,23 @@ object Modules {

// jakarta
val coreJakarta = module(":kotlin-jdsl-core-jakarta")
val reactiveCoreJakarta = module(":kotlin-jdsl-reactive-core-jakarta")
val queryJakarta = module(":kotlin-jdsl-query-jakarta")
val hibernateJakarta = module(":hibernate-kotlin-jdsl-jakarta")
val hibernateReactiveJakarta = module(":hibernate-reactive-kotlin-jdsl-jakarta")

// jakarta spring
val springDataCoreJakarta = module(":spring-data-kotlin-jdsl-core-jakarta")
val springBatchInfrastructureJakarta = module(":spring-batch-kotlin-jdsl-infrastructure-jakarta")
val springDataAutoconfigureJakarta = module(":spring-data-kotlin-jdsl-autoconfigure-jakarta")
val springDataStarterJakarta = module(":spring-data-kotlin-jdsl-starter-jakarta")
val springDataReactiveCoreJakarta = module(":spring-data-kotlin-jdsl-reactive-core-jakarta")
val springDataHibernateReactiveJakarta = module(":spring-data-kotlin-jdsl-hibernate-reactive-jakarta")

val testFixtureHibernateReactiveJakarta = module(":test-fixture-hibernate-reactive-jakarta")
val testFixtureIntegrationJakarta = module(":test-fixture-integration-jakarta")
val testFixtureEntityJakarta = module(":test-fixture-entity-jakarta")
val testFixtureIntegrationReactiveJakarta = module(":test-fixture-integration-reactive-jakarta")

private fun module(name: String): Module = Module(name)
}
Expand Down
34 changes: 34 additions & 0 deletions examples/spring-boot-hibernate-reactive-3/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@Suppress("DSL_SCOPE_VIOLATION")
plugins {
alias(libs.plugins.spring.boot3)
kotlin("plugin.spring")
kotlin("plugin.jpa")
}

apply(plugin = "org.springframework.boot")
apply(plugin = "kotlin-spring")
apply(plugin = "kotlin-jpa")

coverage {
exclude(project)
}

dependencies {
implementation(Modules.queryJakarta)
// implementation("com.linecorp.kotlin-jdsl:spring-data-kotlin-jdsl-hibernate-reactive-jakarta:x.y.z")
implementation(Modules.springDataHibernateReactiveJakarta)
implementation(libs.hibernate.reactive.jakarta)
implementation(libs.coroutine.jdk8)
implementation(libs.coroutine.reactor)
implementation(libs.bundles.mutiny)

implementation(Modules.testFixtureHibernateReactiveJakarta)

implementation(libs.spring.boot.webflux)
implementation(libs.spring.boot3.jpa)
implementation(libs.jackson.kotlin.module)
implementation(libs.h2)
implementation(platform(libs.spring.boot3.bom))

testImplementation(libs.spring.boot3.test)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.linecorp.kotlinjdsl.spring.data.example

import com.linecorp.kotlinjdsl.querydsl.expression.col
import com.linecorp.kotlinjdsl.spring.data.example.entity.Book
import com.linecorp.kotlinjdsl.spring.data.reactive.query.SpringDataHibernateMutinyReactiveQueryFactory
import com.linecorp.kotlinjdsl.spring.data.reactive.query.listQuery
import com.linecorp.kotlinjdsl.spring.data.reactive.query.singleQuery
import org.hibernate.reactive.mutiny.Mutiny
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Service
import org.springframework.web.bind.annotation.*
import reactor.core.publisher.Mono
import java.util.concurrent.CompletionStage

@RestController
@RequestMapping("/api/v1/books")
class BookController(
private val bookService: BookService,
) {
@PostMapping
fun createBook(@RequestBody spec: BookService.CreateBookSpec): Mono<ResponseEntity<Long>> =
Mono.fromCompletionStage { bookService.create(spec) }.map { ResponseEntity.ok().body(it.id) }

@GetMapping("/{bookId}")
suspend fun findById(@PathVariable bookId: Long): ResponseEntity<Book> {
val book = bookService.findById(bookId)

return ResponseEntity.ok(book)
}

@GetMapping
suspend fun findAll(@RequestParam("name") name: String): ResponseEntity<List<Book>> {
val books = bookService.findAll(BookService.FindBookSpec(name = name))

return ResponseEntity.ok(books)
}
}

@Service
class BookService(
private val mutinySessionFactory: Mutiny.SessionFactory,
private val queryFactory: SpringDataHibernateMutinyReactiveQueryFactory,
) {
fun create(spec: CreateBookSpec): CompletionStage<Book> {
val book = Book(name = spec.name)
return mutinySessionFactory.withSession { session -> session.persist(book).flatMap { session.flush() } }
.map { book }
.subscribeAsCompletionStage()
}

suspend fun findById(id: Long): Book {
return queryFactory.singleQuery {
select(entity(Book::class))
from(entity(Book::class))
where(col(Book::id).equal(id))
}
}

suspend fun findAll(spec: FindBookSpec): List<Book> {
return queryFactory.listQuery {
select(entity(Book::class))
from(entity(Book::class))
where(col(Book::name).like("%${spec.name}%"))
}
}

data class CreateBookSpec(
val name: String
) {
fun toJson() = """{"name":"$name"}"""
}

data class FindBookSpec(
val name: String
) {
fun toJson() = """{"name":"$name"}"""
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.linecorp.kotlinjdsl.spring.data.example

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication

@SpringBootApplication
class ExampleApplication

fun main(args: Array<String>) {
runApplication<ExampleApplication>(*args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.linecorp.kotlinjdsl.spring.data.example

import com.linecorp.kotlinjdsl.query.creator.SubqueryCreatorImpl
import com.linecorp.kotlinjdsl.spring.data.reactive.query.SpringDataHibernateMutinyReactiveQueryFactory
import org.hibernate.reactive.mutiny.Mutiny.SessionFactory
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import jakarta.persistence.EntityManagerFactory
import jakarta.persistence.Persistence

@Configuration
class QueryFactoryConfiguration {
@Bean
fun entityManagerFactory(): EntityManagerFactory = Persistence.createEntityManagerFactory("book")

@Bean
fun mutinySessionFactory(entityManagerFactory: EntityManagerFactory): SessionFactory =
entityManagerFactory.unwrap(SessionFactory::class.java)
.apply {
withSession {
// currently H2 db does not support officially
// and does not allow extract & create schema with h2 db in hibernate-reactive
// so DDL query execute directly
it.createNativeQuery<Int>(
"""
create table book (
id bigint not null auto_increment,
name varchar(255),
primary key (id)
)
""".trimIndent()
).executeUpdate()
}.subscribeAsCompletionStage().get()
}

@Bean
fun queryFactory(sessionFactory: SessionFactory): SpringDataHibernateMutinyReactiveQueryFactory {
return SpringDataHibernateMutinyReactiveQueryFactory(
sessionFactory = sessionFactory,
subqueryCreator = SubqueryCreatorImpl()
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.linecorp.kotlinjdsl.spring.data.example.entity

import jakarta.persistence.*

@Entity
@Table(name = "book")
data class Book(
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0,

@Column
val name: String,
) {
fun toJson() = """{"id":$id,"name":"$name"}"""
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.2" xmlns="http://xmlns.jcp.org/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence
http://xmlns.jcp.org/xml/ns/persistence/persistence_3_1.xsd">

<persistence-unit name="book">
<provider>org.hibernate.reactive.provider.ReactivePersistenceProvider</provider>
<class>com.linecorp.kotlinjdsl.spring.data.example.entity.Book</class>

<properties>
<property name="hibernate.vertx.pool.connect_timeout" value="30000"/>
<property name="hibernate.vertx.pool.class" value="com.linecorp.kotlinjdsl.vertx.configuration.H2ConnectionPool"/>
<property name="hibernate.vertx.pool.configuration_class" value="com.linecorp.kotlinjdsl.vertx.configuration.VertxH2DBConnectionPoolConfiguration"/>
<property name="jakarta.persistence.jdbc.url" value="jdbc:h2:mem:~/test;MODE=MYSQL;DATABASE_TO_LOWER=TRUE;DATABASE_TO_UPPER=false"/>
<property name="jakarta.persistence.jdbc.user" value="sa"/>
<property name="jakarta.persistence.jdbc.password" value=""/>
<property name="jakarta.persistence.jdbc.driver" value="org.h2.Driver"/>
<property name="hibernate.dialect" value="org.hibernate.dialect.MySQL8Dialect"/>
<property name="hibernate.show_sql" value="true"/>
<property name="hibernate.format_sql" value="true"/>
<property name="hibernate.hbm2ddl.auto" value="none"/>
<property name="hibernate.use_sql_comments" value="true"/>
</properties>
</persistence-unit>

</persistence>
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package com.linecorp.kotlinjdsl.spring.data.example

import com.linecorp.kotlinjdsl.spring.data.example.entity.Book
import org.assertj.core.api.WithAssertions
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.web.reactive.server.WebTestClient

@SpringBootTest
@AutoConfigureWebTestClient(timeout = "100000")
internal class BookControllerIntegrationTest : WithAssertions {
@Autowired
private lateinit var client: WebTestClient

private val context = "/api/v1/books"

@Test
fun createBook() {
createBook(BookService.CreateBookSpec("name"))
}

@Test
fun findById() {
val spec = BookService.CreateBookSpec("name1")
val id = createBook(spec)
client.get().uri("$context/$id")
.exchange()
.expectStatus().isOk
.expectBody(Book::class.java)
.value {
assertThat(Book(id = id, name = spec.name)).isEqualTo(it)
}

}

@Test
fun findByName() {
val spec1 = BookService.CreateBookSpec("name2")
val spec2 = BookService.CreateBookSpec("name2")
val id1 = createBook(spec1)
val id2 = createBook(spec2)
client.get().uri(context + "?name=${spec1.name}")
.exchange()
.expectStatus().isOk
.expectBodyList(Book::class.java)
.contains(Book(id = id1, name = spec1.name), Book(id = id2, name = spec2.name))
}

private fun createBook(spec: BookService.CreateBookSpec) = client.post().uri(context)
.bodyValue(spec)
.exchange()
.expectStatus().isOk
.returnResult(Long::class.java)
.responseBody
.blockFirst()!!
}
23 changes: 23 additions & 0 deletions hibernate-reactive-jakarta/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
apply<PublishPlugin>()

coverage {
exclude(project)
}

dependencies {
api(Modules.reactiveCoreJakarta)

compileOnly(libs.hibernate.reactive.jakarta)
compileOnly(libs.slf4j)
compileOnly(libs.bundles.mutiny)

testImplementation(libs.bundles.mutiny)
testImplementation(libs.coroutine.jdk8)
testImplementation(Modules.testFixtureIntegrationReactiveJakarta)
testImplementation(Modules.testFixtureHibernateReactiveJakarta)
testImplementation(libs.hibernate.reactive.jakarta)
testImplementation(libs.kotlin.reflect)
testImplementation(libs.h2)
testImplementation(libs.agroal.pool)
testImplementation(libs.vertx.jdbc.client)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package com.linecorp.kotlinjdsl.query

import io.smallrye.mutiny.coroutines.awaitSuspending
import org.hibernate.reactive.mutiny.Mutiny
import jakarta.persistence.Parameter
import kotlin.reflect.KClass

class HibernateMutinyReactiveQuery<R>(private val query: Mutiny.Query<R>) : ReactiveQuery<R> {
override suspend fun singleResult(): R = query.singleResult.awaitSuspending()
override suspend fun resultList(): List<R> = query.resultList.awaitSuspending()
override suspend fun singleResultOrNull(): R? = query.singleResultOrNull.awaitSuspending()
override suspend fun executeUpdate(): Int = query.executeUpdate().awaitSuspending()

override fun setParameter(position: Int, value: Any?): ReactiveQuery<R> {
query.setParameter(position, value)
return this
}

override fun setParameter(name: String, value: Any?): ReactiveQuery<R> {
query.setParameter(name, value)
return this
}

override fun <T> setParameter(parameter: Parameter<T>, value: T?): ReactiveQuery<R> {
query.setParameter(parameter, value)
return this
}

override fun setMaxResults(maxResults: Int): ReactiveQuery<R> {
query.maxResults = maxResults
return this
}

override fun setFirstResult(firstResult: Int): ReactiveQuery<R> {
query.firstResult = firstResult
return this
}
override fun setQueryHint(hintName: String, value: Any) {
throw UnsupportedOperationException("Hibernate-reactive does not support JPA query hint yet. if hibernate-reactive setQueryHint method support officially please let us know. we will fix it")
}

override val maxResults: Int
get() = query.maxResults
override val firstResult: Int
get() = query.firstResult

@Suppress("UNCHECKED_CAST")
override fun <T : Any> unwrap(type: KClass<T>): T {
return query as T
}
}
Loading

0 comments on commit afcdcdb

Please sign in to comment.