Skip to content

Commit 0b9c0d1

Browse files
authored
#45 Setup JOOQ and R2DBC foundation (#53)
1 parent c6e07b4 commit 0b9c0d1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

43 files changed

+532
-186
lines changed

backend/buildSrc/build.gradle.kts

+5
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,18 @@ repositories {
99
}
1010

1111
dependencies {
12+
val jooqVersion = "3.19.15"
13+
1214
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlin.coreLibrariesVersion}")
1315
implementation("org.jetbrains.kotlin:kotlin-allopen:${kotlin.coreLibrariesVersion}")
1416

1517
implementation("io.gitlab.arturbosch.detekt:detekt-gradle-plugin:1.23.7")
1618

1719
implementation("org.openapitools:openapi-generator-gradle-plugin:7.9.0")
1820

21+
implementation("org.jooq:jooq-codegen:$jooqVersion")
22+
implementation("org.jooq:jooq-codegen-gradle:$jooqVersion")
23+
1924
implementation("org.springframework.boot:spring-boot-gradle-plugin:3.3.5")
2025
implementation("io.spring.gradle:dependency-management-plugin:1.1.6")
2126
}
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,6 @@
11
plugins {
22
id("buildlogic.spring-conventions")
3-
id("org.openapi.generator")
3+
id("buildlogic.oapi-server-conventions")
4+
id("buildlogic.jooq-conventions")
45
application
56
}
6-
7-
extra["generateOAPIServer"] = { serviceName: String ->
8-
val apiResourcesDir = layout.projectDirectory.asFile.let { "$it/src/main/resources" }
9-
val generatedDir = layout.buildDirectory.dir("generated").get().toString()
10-
11-
openApiGenerate {
12-
generatorName = "kotlin-spring"
13-
inputSpec = "$apiResourcesDir/static/openapi/api.yml"
14-
outputDir = generatedDir
15-
invokerPackage = "$group.$serviceName"
16-
apiPackage = "$group.$serviceName.api.generated"
17-
modelPackage = "$group.$serviceName.model.generated"
18-
configOptions = mapOf(
19-
"delegatePattern" to "true",
20-
"useSpringBoot3" to "true",
21-
)
22-
}
23-
24-
sourceSets {
25-
main {
26-
kotlin {
27-
srcDir("$generatedDir/src/main/kotlin")
28-
}
29-
}
30-
}
31-
32-
tasks.compileKotlin.configure {
33-
dependsOn("openApiGenerate")
34-
}
35-
36-
application {
37-
mainClass = "$group.$serviceName.ApplicationKt"
38-
}
39-
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
plugins {
2+
id("buildlogic.kotlin-library-conventions")
3+
id("org.jooq.jooq-codegen-gradle")
4+
}
5+
6+
extra["generateJOOQ"] = { serviceName: String ->
7+
val jooqVersion = "3.19.15"
8+
val testContainersVersion = "1.20.3"
9+
10+
val generatedDir = "$projectDir/build/generated"
11+
val jooqGeneratedDir = "$generatedDir/jooq/src/main/kotlin"
12+
13+
dependencies {
14+
jooqCodegen("jakarta.xml.bind:jakarta.xml.bind-api:4.0.2")
15+
jooqCodegen("org.jooq:jooq-meta-extensions:$jooqVersion")
16+
jooqCodegen("org.jooq:jooq-meta-kotlin:$jooqVersion")
17+
jooqCodegen("org.postgresql:postgresql:42.7.4")
18+
jooqCodegen("org.testcontainers:postgresql:$testContainersVersion")
19+
jooqCodegen("org.testcontainers:testcontainers:$testContainersVersion")
20+
}
21+
22+
sourceSets {
23+
main {
24+
java {
25+
srcDir(jooqGeneratedDir)
26+
}
27+
}
28+
}
29+
30+
jooq {
31+
executions {
32+
create("main") {
33+
val jdbcUrl = {
34+
val schemaSql = "$projectDir/src/main/resources/database/changelog.sql"
35+
val protocol = "jdbc:tc:postgresql:16"
36+
val tmpfs = "TC_TMPFS=/testtmpfs:rw&amp"
37+
val script = "TC_INITSCRIPT=file:$schemaSql"
38+
"$protocol:///test?$tmpfs;$script"
39+
}
40+
41+
configuration {
42+
logging = org.jooq.meta.jaxb.Logging.DEBUG
43+
jdbc {
44+
driver = "org.testcontainers.jdbc.ContainerDatabaseDriver"
45+
url = jdbcUrl()
46+
username = "postgres"
47+
password = "postgres"
48+
}
49+
generator {
50+
name = "org.jooq.codegen.KotlinGenerator"
51+
database {
52+
name = "org.jooq.meta.postgres.PostgresDatabase"
53+
inputSchema = serviceName
54+
includes = ".*"
55+
}
56+
generate {
57+
isImmutablePojos = true
58+
isPojosAsKotlinDataClasses = true
59+
isKotlinNotNullPojoAttributes = true
60+
isKotlinNotNullRecordAttributes = true
61+
isImplicitJoinPathsToMany = false
62+
}
63+
target {
64+
directory = jooqGeneratedDir
65+
}
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
tasks.compileKotlin.configure {
73+
dependsOn(tasks.jooqCodegen)
74+
}
75+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
plugins {
2+
id("buildlogic.kotlin-library-conventions")
3+
id("org.openapi.generator")
4+
application
5+
}
6+
7+
extra["generateOAPIServer"] = { serviceName: String ->
8+
val apiResourcesDir = layout.projectDirectory.asFile.let { "$it/src/main/resources" }
9+
val generatedDir = layout.buildDirectory.dir("generated").get().toString()
10+
11+
openApiGenerate {
12+
generatorName = "kotlin-spring"
13+
inputSpec = "$apiResourcesDir/static/openapi/api.yml"
14+
outputDir = generatedDir
15+
invokerPackage = "$group.$serviceName"
16+
apiPackage = "$group.$serviceName.api.generated"
17+
modelPackage = "$group.$serviceName.model.generated"
18+
modelNameSuffix = "Message"
19+
configOptions = mapOf(
20+
"delegatePattern" to "true",
21+
"useSpringBoot3" to "true",
22+
"reactive" to "true",
23+
)
24+
}
25+
26+
sourceSets {
27+
main {
28+
kotlin {
29+
srcDir("$generatedDir/src/main/kotlin")
30+
}
31+
}
32+
}
33+
34+
tasks.compileKotlin.configure {
35+
dependsOn("openApiGenerate")
36+
}
37+
38+
application {
39+
mainClass = "$group.$serviceName.ApplicationKt"
40+
}
41+
}

backend/config/detekt.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ style:
178178
maxChainedCalls: 3
179179
MaxLineLength:
180180
active: true
181-
maxLineLength: 80
181+
maxLineLength: 100
182182
MultilineLambdaItParameter:
183183
active: true
184184
MultilineRawStringIndentation:

backend/config/env/local.sh

100644100755
+1-2
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
1-
cd "$(dirname "$0")"
2-
export $(cat .env | xargs)
1+
export $(cat "$(dirname "$0")/.env" | xargs)

backend/foundation-test/build.gradle.kts

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ plugins {
44

55
dependencies {
66
api(libs.org.springframework.boot.spring.boot.starter.test)
7-
api(libs.org.testcontainers.postgresql)
7+
88
api(libs.junit.junit)
9+
api(libs.org.testcontainers.postgresql)
10+
api(libs.org.testcontainers.r2dbc)
11+
api(libs.org.testcontainers.junit.jupiter)
12+
api(libs.io.projectreactor.reactor.test)
913
}

backend/foundation-test/src/main/kotlin/ru/ifmo/se/dating/container/Postgres.kt

+11-6
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import org.testcontainers.containers.PostgreSQLContainer
55
class Postgres private constructor() : AutoCloseable {
66
private val container = PostgreSQLContainer(DOCKER_IMAGE_NAME)
77

8-
fun jdbcUrl(): String =
9-
container.jdbcUrl
8+
private val host: String get() = container.host
109

11-
fun username(): String =
12-
container.username
10+
private val port: Int get() = container.firstMappedPort
1311

14-
fun password(): String =
15-
container.password
12+
private val databaseName: String get() = container.databaseName
13+
14+
val jdbcUrl: String get() = container.jdbcUrl
15+
16+
val r2dbcUrl: String get() = "r2dbc:postgresql://$host:$port/$databaseName"
17+
18+
val username: String get() = container.username
19+
20+
val password: String get() = container.password
1621

1722
override fun close() {
1823
container.stop()

backend/foundation/build.gradle.kts

+21-9
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,35 @@
11
plugins {
22
id("buildlogic.kotlin-library-conventions")
3+
id("buildlogic.jooq-conventions")
34
}
45

56
dependencies {
7+
api(libs.org.springframework.boot.spring.boot)
8+
api(libs.org.springframework.boot.spring.boot.starter.webflux)
9+
api(libs.org.springframework.boot.spring.boot.starter.data.r2dbc)
10+
11+
api(libs.org.springframework.spring.web)
12+
api(libs.org.springframework.spring.context)
13+
14+
api(libs.org.springdoc.springdoc.openapi.starter.webflux.ui)
15+
616
api(libs.io.swagger.core.v3.swagger.annotations)
717
api(libs.io.swagger.core.v3.swagger.models)
818
api(libs.org.openapitools.jackson.databind.nullable)
9-
api(libs.org.springdoc.springdoc.openapi.starter.webmvc.ui)
1019

11-
api(libs.org.springframework.spring.web)
12-
api(libs.org.springframework.spring.context)
20+
api(libs.jakarta.validation.jakarta.validation.api)
21+
api(libs.com.fasterxml.jackson.core.jackson.databind)
1322

14-
api(libs.org.springframework.boot.spring.boot)
15-
api(libs.org.springframework.boot.spring.boot.starter.web)
23+
api(libs.org.jetbrains.kotlinx.kotlinx.coroutines.reactor)
24+
api(libs.io.projectreactor.kotlin.reactor.kotlin.extensions)
25+
26+
api(libs.org.jooq.jooq)
27+
api(libs.org.jooq.jooq.kotlin)
28+
api(libs.org.jooq.jooq.meta.extensions)
29+
api(libs.org.jooq.jooq.meta.kotlin)
1630

17-
api(libs.org.springframework.boot.spring.boot.starter.jdbc)
1831
api(libs.org.liquibase.liquibase.core)
1932
api(libs.org.postgresql.postgresql)
20-
21-
api(libs.com.fasterxml.jackson.core.jackson.databind)
22-
api(libs.jakarta.validation.jakarta.validation.api)
33+
api(libs.org.postgresql.r2dbc.postgresql)
2334
}
35+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package ru.ifmo.se.dating.exception
2+
3+
open class GenericException(message: String, cause: Throwable? = null) :
4+
Exception(message, cause)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package ru.ifmo.se.dating.exception
2+
3+
class InvalidValueException(string: String, cause: Throwable? = null) :
4+
GenericException(string, cause)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package ru.ifmo.se.dating.exception
2+
3+
class NotFoundException(message: String, cause: Throwable? = null) :
4+
GenericException("Not found: $message", cause)
5+
6+
fun <T> T?.orThrowNotFound(message: String): T =
7+
this ?: throw NotFoundException(message)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
package ru.ifmo.se.dating.exception
2+
3+
open class StorageException(string: String, cause: Throwable? = null) :
4+
GenericException(string, cause)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package ru.ifmo.se.dating.spring
2+
3+
import io.r2dbc.spi.Connection
4+
import kotlinx.coroutines.flow.Flow
5+
import kotlinx.coroutines.reactive.asFlow
6+
import kotlinx.coroutines.reactive.awaitSingle
7+
import kotlinx.coroutines.reactor.awaitSingleOrNull
8+
import org.jooq.Publisher
9+
import org.jooq.SQLDialect
10+
import org.jooq.conf.Settings
11+
import org.jooq.exception.IntegrityConstraintViolationException
12+
import org.jooq.impl.DSL
13+
import org.springframework.r2dbc.core.DatabaseClient
14+
import org.springframework.stereotype.Component
15+
import reactor.core.publisher.Flux
16+
import reactor.core.publisher.Mono
17+
import reactor.kotlin.core.publisher.toFlux
18+
import ru.ifmo.se.dating.storage.jooq.DSLBlock
19+
import ru.ifmo.se.dating.storage.jooq.JooqDatabase
20+
import ru.ifmo.se.dating.storage.jooq.exception.toStorage
21+
22+
@Component
23+
class SpringJooqDatabase(private val database: DatabaseClient) : JooqDatabase {
24+
private val settings = Settings()
25+
.withBindOffsetDateTimeType(true)
26+
.withBindOffsetTimeType(true)
27+
28+
override fun <T : Any> flow(block: DSLBlock<T>): Flow<T> =
29+
flux(block).asFlow()
30+
31+
override suspend fun <T : Any> only(block: DSLBlock<T>): T =
32+
mono(block).awaitSingle()
33+
34+
override suspend fun <T : Any> maybe(block: DSLBlock<T>): T? =
35+
mono(block).awaitSingleOrNull()
36+
37+
private fun Connection.dsl() =
38+
DSL.using(this, SQLDialect.POSTGRES, settings)
39+
40+
private fun <T : Any> flux(block: DSLBlock<T>) = database
41+
.inConnectionMany { block(it.dsl()).toFlux() }
42+
.onErrorMap(IntegrityConstraintViolationException::class.java) { it.toStorage() }
43+
44+
private fun <T : Any> mono(block: DSLBlock<T>) = database
45+
.inConnection { block(it.dsl()).toMono() }
46+
.onErrorMap(IntegrityConstraintViolationException::class.java) { it.toStorage() }
47+
}
48+
49+
private fun <T> publisher(jooq: Publisher<T>) =
50+
jooq as org.reactivestreams.Publisher<T>
51+
52+
private fun <T> Publisher<T>.toFlux(): Flux<T> =
53+
Flux.from(publisher(this))
54+
55+
private fun <T> Publisher<T>.toMono(): Mono<T> =
56+
Mono.from(publisher(this))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package ru.ifmo.se.dating.spring
2+
3+
import kotlinx.coroutines.flow.Flow
4+
import org.springframework.stereotype.Component
5+
import org.springframework.transaction.ReactiveTransactionManager
6+
import org.springframework.transaction.TransactionDefinition
7+
import org.springframework.transaction.reactive.TransactionalOperator
8+
import org.springframework.transaction.reactive.executeAndAwait
9+
import org.springframework.transaction.reactive.transactional
10+
import org.springframework.transaction.support.DefaultTransactionDefinition
11+
import ru.ifmo.se.dating.storage.TxEnv
12+
13+
@Component
14+
class SpringTxEnv(transactionManager: ReactiveTransactionManager) : TxEnv {
15+
private val operator = DefaultTransactionDefinition().apply {
16+
isolationLevel = TransactionDefinition.ISOLATION_SERIALIZABLE
17+
}.let { TransactionalOperator.create(transactionManager, it) }
18+
19+
override suspend fun <T : Any> transactional(action: suspend () -> T): T =
20+
operator.executeAndAwait { action() }
21+
22+
override fun <T : Any> transactional(flow: Flow<T>): Flow<T> =
23+
flow.transactional(operator)
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package ru.ifmo.se.dating.storage
2+
3+
internal enum class FetchPolicy {
4+
SNAPSHOT,
5+
WRITE_LOCKED,
6+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package ru.ifmo.se.dating.storage
2+
3+
import kotlinx.coroutines.flow.Flow
4+
5+
interface TxEnv {
6+
suspend fun <T : Any> transactional(action: suspend () -> T): T
7+
fun <T : Any> transactional(flow: Flow<T>): Flow<T>
8+
}

0 commit comments

Comments
 (0)