Особенности jOOQ

В этой заметке разберу 2 момента, которые мне не нравятся в jOOQ.

jOOQ, Quarkus и реактивность

  • jOOQ поддерживает реактивность через R2DBC
  • Quarkus поддерживает реактивность через Vert.x

Кто-то из них должен уступить. Есть специальный генератор jOOQ c Vert.x и пример – но он не обновлялся уже год (чуть-чуть устарел). Нужно попробовать, поразбираться как оно.

Хороший вопрос на эту тему: https://stackoverflow.com/questions/72669626/using-jooq-with-reactive-sql-client-in-quarkus

В принципе, многие считают, что нереактивные запросы к БД – это не проблема. Сам не тестировал, но многие годы и без этого работал. Понятно, что будет работать, но хотелось бы уж реактивности.

jOOQ, Spring и реактивность

Казалось бы: что может пойти не так и здесь?

Автоматически не заводится, ConnectionFactory должно быть задано вручную:

@Configuration  
@EnableR2dbcRepositories  
class R2dbcConfig(  
  @Value("\${spring.r2dbc.url}") private val dsUrl: String,  
  @Value("\${spring.r2dbc.username}") private val dsUsername: String,  
  @Value("\${spring.r2dbc.password}") private val dsPassword: String,  
) : AbstractR2dbcConfiguration() {  
  
  override fun connectionFactory(): ConnectionFactory {  
    return ConnectionFactories.get(  
      ConnectionFactoryOptions  
        .parse(dsUrl)  
        .mutate()  
        .option(ConnectionFactoryOptions.USER, dsUsername)  
        .option(ConnectionFactoryOptions.PASSWORD, dsPassword)  
        .build(),  
      )  
  }  
}

Плюс, реактивно работает так (через Flux.from):

fun getAllEmails(): Flow<String?> {
  return Flux.from(db.select(USERS.EMAIL).from(USERS))
    .asFlow()
    .map { it.value1() }
}

Но не так (через fetchAsync):

suspend fun getAll(): List<UsersRecord> {  
  return jdbcDb.fetchAsync(USERS).await()  
}

Из-за этого приходится делать 2 DSLContext:

@Primary  
@Bean  
fun dslContext(r2dbcConfig: R2dbcConfig): DSLContext {
  return DSL.using(r2dbcConfig.connectionFactory())
}
  
@Bean("jdbcDb")  
fun jdbcDslContext(dataSource: HikariDataSource): DSLContext {  
  return DSL.using(dataSource.connection)  
}

Все еще непонятно почему fetchAsync не работает через R2DBC, но можно записать в таком виде (кода немного больше, но работает):

suspend fun byIdNullable(id: Long): UsersRecord? {  
  return Mono.from(  
    db.select(USERS).from(USERS).where(USERS.ID.eq(id)),  
  ).map { it.value1() }.awaitSingleOrNull()  
}

Т.к. jOOQ для сложных случаев, то немного больше кода вполне приемлемо.

jOOQ, Kotlin и null-значения

  • разработчики jOOQ утверждают, что несмотря на non-null поля в таблице вполне возможно написать запрос так, что вернется null (что правда).
  • только из-за этого все поля всегда nullable.
  • и во всех случаях вручную нужно, условно говоря, писать !!

Как по мне, все-таки лучше делать с учетом нульности колонок, а специфичные случаи как раз можно отдельно рассматривать. Но такого сейчас пока что нет – неудобно.

Выводы

В связи с этим, на практике имеет смысл совмещать 2 движка доступа с Postgres: Spring Data Repositories (простые случаи) & jOOQ (сложные случаи).

Отдельно отмечу, что Flyway не работает с R2DBC и ему нужны JDBC-настройки (spring.datasource).