Ниссан jooq

от автора

Это небольшая история о том как я настраивал 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 и это все успешно применяется. Хотя для пэт-проектов разница небольшая, жук в использовании для меня намного удобнее.
Вот кстати приложение, для которого я это все делал
Подписывайтесь на телегу, я там иногда пощщу что-то по теме.

На этом все.


ссылка на оригинал статьи https://habr.com/ru/articles/839680/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *