Это небольшая история о том как я настраивал jooq генератор с gradle и postgis. По большому счету в моем пэт проекте это было не особо нужно, но хотелось повторить стэк, с которым работаю, но с нуля.
Для начала все что касаеся бд я вынес в библиотеку. Тут лежат грэдл файл, миграции и сгенерированные жук классы. Грэдл файл выглядит как-то так.
plugins {
val kotlinVersion = "1.9.20"
id("java")
id("org.flywaydb.flyway") version "9.10.2"
id("nu.studer.jooq") version ("6.0.1")
kotlin("jvm") version kotlinVersion
}
group = "ru.mapgramm"
version = "0.0.1-SNAPSHOT"
repositories {
mavenCentral()
}
dependencies {
jooqGenerator("org.postgresql:postgresql")
implementation("org.jooq:jooq")
implementation("org.postgis:postgis-jdbc")
compileOnly("org.postgresql:postgresql")
jooqGenerator("org.postgresql:postgresql")
}
Сначала я решил просто прокатывать локально миграции и запускать генератор на локально запущенной базе данных.
Собственно, приступаем к настройке генератора. Быстрым поиском по интернету нахожу настройку, адаптирую ее по котлин грэдл и запускаю.
jooq {
edition.set(nu.studer.gradle.jooq.JooqEdition.OSS)
configurations {
create("main") {
jooqConfiguration.apply {
jdbc.apply {
driver = "org.postgresql.Driver"
url = <jdbc_url>
user = <username>
password = <password>
}
generator.apply {
name = "org.jooq.codegen.DefaultGenerator"
generate.apply {
isDeprecated = false
isRecords = true
isImmutablePojos = false
isFluentSetters = false
isJavaBeansGettersAndSetters = false
}
database.apply {
name = "org.jooq.meta.postgres.PostgresDatabase"
inputSchema = "public"
}
target.apply {
packageName = "ru.keykeeper.core.api.jooq.generated"
directory = "$projectDir/src/main/java/"
}
strategy.name = "org.jooq.codegen.DefaultGeneratorStrategy"
}
}
}
}
}
Как итог - получаем какое-то огромное количество классов. Они появились из-за плагина постгис и пока не очень понятно, как от них избавиться. Хотя они не очень то и мешают.
Попробуем написать какой-никакой запрос. Вот, например, запрос на получение все групп с джоином к постам и фильтром по зуму
fun findAllByZoom(zoom: Int, dsl: DSLContext = jooq): List<PostGroupDto> {
val postGroupRecords = dsl
.select(*POST_GROUP_VIEW.fields())
.select(*POST.fields())
.from(POST_GROUP_VIEW)
.join(POST_TO_GROUP).on(POST_TO_GROUP.GROUP_ID.eq(POST_GROUP_VIEW.ID))
.join(POST).on(POST_TO_GROUP.POST_ID.eq(POST.ID))
.where(POST_GROUP_VIEW.ZOOM.eq(zoom))
.fetchGroups(
{ r -> r.into(PostGroupViewRecord::class.java) },
{ r -> r.into(PostRecord::class.java) },
)
return postGroupRecords.entries.map { entry ->
entry.key.toDto(entry.value.map { it.toDto() })
}
}
В целом оно работает и то что это все джава классы совершенно не мешает, но есть некоторые другие проблемы.
Здесь вставка строк запрещена.
Я не могу сохранить дату. И jsonb. И geometry. И вообще, что-то не так с сохранением, если это не строка или число. Выглядит это так:
org.postgresql.util.PSQLException: ERROR: column "payload" is of type jsonb but expression is of type character varying
И вот на это я трачу какое-то неприличное количество времени. За это время я нашел как генерировать котлин классы и как нормально генерировать во время сборки, но об этом чуть позже. А решением проблемы был простой советский...
@Bean
fun configuration(connectionProvider: DataSourceConnectionProvider): DefaultConfiguration {
val jooqConfiguration = DefaultConfiguration()
jooqConfiguration.set(connectionProvider)
-> jooqConfiguration.setSQLDialect(SQLDialect.POSTGRES);
return jooqConfiguration
}
... setSQLDialect(SQLDialect.POSTGRES)
, как оказалось, о настройках самого жука тоже не стоит забывать.
@Configuration
class JooqConfiguration {
@Bean(value = ["jdbcDSLContext"])
fun jdbcDSLContext(configuration: DefaultConfiguration): DSLContext {
return DefaultDSLContext(configuration)
}
@Bean
fun connectionProvider(dataSource: DataSource): DataSourceConnectionProvider {
return DataSourceConnectionProvider(
TransactionAwareDataSourceProxy(dataSource),
)
}
@Bean
fun configuration(connectionProvider: DataSourceConnectionProvider): DefaultConfiguration {
val jooqConfiguration = DefaultConfiguration()
jooqConfiguration.set(connectionProvider)
jooqConfiguration.setSQLDialect(SQLDialect.POSTGRES);
return jooqConfiguration
}
}
Генерируй правильно
Пока я искал решение проблемы выше, я нашел как можно удачно генерировать классы жука с flyway и testcontainers. Я примерно так и представлял это себе, но тут уже кто-то придумал за меня, так что воспользуемся.
Сперва сделал для себя класс с контейнером postgis, так как в тестконтейнерах такой не нашел. Плюс, настроил миграцию flyway в этот контейнер. Плюс, проставил эти проперти в генераторе.
abstract class PostgresService : BuildService<BuildServiceParameters.None>, AutoCloseable {
private var image = org.testcontainers.utility.DockerImageName.parse("postgis/postgis:12-3.0")
.asCompatibleSubstituteFor("postgres")
private val container = org.testcontainers.containers.PostgreSQLContainer(image)
init { container.start() }
override fun close() = container.stop()
fun getContainer() = container
}
val dbContainerProvider: Provider<PostgresService> = project.gradle.sharedServices
.registerIfAbsent("postgres", PostgresService::class) {}
flyway {
val dbContainer = dbContainerProvider.get().getContainer()
url = dbContainer.jdbcUrl
user = dbContainer.username
password = dbContainer.password
locations = arrayOf("classpath:db/migration")
sqlMigrationPrefix = "V"
}
jooq {
val dbContainer = dbContainerProvider.get().getContainer()
...
jdbc.apply {
driver = "org.postgresql.Driver"
url = dbContainer.jdbcUrl
user = dbContainer.username
password = dbContainer.password
}
...
}
И тут же нужно проставить зависимость задачи на генерацию классов от миграции бд.
val generateJooq by project.tasks
generateJooq.dependsOn("flywayMigrate")
Очень важный заголовок
generator.apply {
name = "org.jooq.codegen.KotlinGenerator"
}
Скрытый текст
Для генерация котлин классов надо поменять значение name
генератора и перезапустить генерацию. В целом очевидно. Но я же сказал что покажу как я это сделал.
Раз параграф короткий, вот мем
Проблема postgis
Остается последняя проблема, я не могу писать запросы с функциями postgis с помощью jooqDsl, а мне надо!
Для jooq-генератора можно настраивать биндинги, чтобы для типов в базе использовались заданные kotlin-типы. Например, вот настройки для jsonb и даты-времени
...
database.apply {
...
withForcedTypes(
org.jooq.meta.jaxb.ForcedType()
.withName(org.jooq.impl.SQLDataType.INSTANT.typeName)
.withIncludeTypes("(?i:TIMESTAMP\\ (WITH|WITHOUT)\\ TIME\\ ZONE)"),
org.jooq.meta.jaxb.ForcedType()
.withName(org.jooq.impl.SQLDataType.JSONB.typeName)
.withIncludeTypes("jsonb"),
)
}
...
Так вот, такие биндинги можно писать самостоятельно для своих типов. Но он получается довольно большой и для всех классов postgis-а делать это не хотелось бы. И как оказалось, есть проект, в котором это сделали за меня.
implementation("io.github.oshai:jooq-postgis-spatial:1.0")
Теперь добавляем биндинг в настройки генератора:
...
database.apply {
...
withCustomTypes(
org.jooq.meta.jaxb.CustomType()
.withName("Geometry")
.withBinding("net.dmitry.jooq.postgis.spatial.binding.JTSGeometryBinding")
.withType("com.vividsolutions.jts.geom.Geometry")
)
withForcedTypes(
...
org.jooq.meta.jaxb.ForcedType()
.withName("Geometry")
.withIncludeTypes("(geometry|GEOMETRY)"),
)
}
...
И вот с ними странная вещь, функция withCustomTypes
- устаревшая и нужно использовать только withForcedTypes
. Но когда я настроил все по-новому, оно перестало работать. Меня устраивает и вариант выше, но если вы знаете, что с этим не так, буду рад совету)
withForcedTypes(
...
org.jooq.meta.jaxb.ForcedType()
.withName("Geometry")
.withTypes("com.vividsolutions.jts.geom.Geometry")
.withBinding("net.dmitry.jooq.postgis.spatial.binding.JTSGeometryBinding")
.withIncludeTypes("(geometry|GEOMETRY)")
)
Ну и сразу пробую сделать запрос с новенькими функциями
dsl
.selectFrom(POST_GROUP)
.where(
POST_GROUP.ZOOM.eq(zoom)
.and(
stContains(
geom1 = JTS.getDefaultGeomFactory().createPolygon(
arrayOf(
Coordinate(bounds.upLeft.x, bounds.upLeft.y),
Coordinate(bounds.upRight.x, bounds.upRight.y),
Coordinate(bounds.downRight.x, bounds.downRight.y),
Coordinate(bounds.downLeft.x, bounds.downLeft.y),
Coordinate(bounds.upLeft.x, bounds.upLeft.y),
)
),
geom2 = POST_GROUP.POINT,
)
)
)
.fetch(mapper)
Итог
В целом все получилось, есть генератор, есть dsl и это все успешно применяется. Хотя для пэт-проектов разница небольшая, жук в использовании для меня намного удобнее.
Вот кстати приложение, для которого я это все делал
Подписывайтесь на телегу, я там иногда пощщу что-то по теме.
На этом все.
babypowder
у первого слова в заголовке окончание обрезалось