Это небольшая история о том как я настраивал 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/
Добавить комментарий