Опирается на правила: R-JOOQ-CTX-1R-JOOQ-CTX-3 и R-JOOQ-CTX-X1R-JOOQ-CTX-X2 из jOOQ Style Guide → раздел 3. DSLContext.

Важно знать

  • DSLContext — Spring-bean, поднимается стартером spring-boot-starter-jooq. Один на ApplicationContext.
  • Инжектится в репозиторий через конструктор, как любая другая зависимость.
  • Поток-безопасен при иммутабельной Configuration. Singleton + internal cache работает быстрее, чем создание нового на запрос.
  • Settings / RecordMapperProvider кастомизируются через @Bean, не через DSL.using(...) на месте.
  • DSL.using(connection), DSL.using(dataSource) в коде репозитория — запрещены. Spring уже сделал.
  • Хранение Connection в state репозитория — нарушение Spring-проводки. Все ресурсы — через DI.

DSLContext — это «вход» в jOOQ DSL. Из него стартуют все запросы (dslContext.selectFrom(...), dslContext.update(...), etc.). Технически он держит ссылку на Configuration — связку SQL-диалекта, источника соединений, маппинг-настроек, и внутренние кеши (reflection-lookup для record-mapping). Из этого устройства следуют правила: один на приложение, иммутабельный, инжектируется.

Один Spring-bean на ApplicationContext

R-JOOQ-CTX-1: DSLContext — bean, поднимается стартером spring-boot-starter-jooq (из BS-18). В сервисе не пишем @Bean DSLContext dslContext(...) { return DSL.using(...); } — это уже сделал стартер на основе вашего DataSource и JooqProperties.

@Repository
@RequiredArgsConstructor
public class JooqOrderRepository implements OrderRepository {
    private final DSLContext dslContext;
    // ...
}

Spring инжектирует тот самый bean, который стартер собрал. Один инстанс на весь ApplicationContext. Все репозитории сервиса смотрят в один DSLContext.

Поток-безопасность и кеши

R-JOOQ-CTX-2: DSLContext поток-безопасен, если Configuration иммутабельна. Внутри Configuration живут:

  • ConnectionProvider — берёт соединение из пула (HikariCP); пул сам поток-безопасен.
  • SQLDialectPOSTGRES, статическая константа.
  • RecordMapperProvider, RecordUnmapperProvider — стратегии маппинга.
  • Reflection cache — для record-mapping jOOQ один раз парсит структуру POJO (геттеры/сеттеры), потом переиспользует. Кеш — ConcurrentHashMap.

Создавать новый DSLContext на каждый запрос — означает каждый раз сбрасывать reflection-cache. Под нагрузкой это даёт измеримое замедление и лишнее давление на GC. Singleton решает обе проблемы.

Когда не singleton? Когда нужна нестандартная конфигурация для конкретного use-case — например, отдельный диалект для миграции из MS SQL. Но это очень редко, и тогда заводится второй bean с явным @Qualifier, не на лету через DSL.using(...).

Settings и RecordMapperProvider — через @Bean

R-JOOQ-CTX-3: если нужны кастомные Settings (например, отключить execute-logging в проде) или RecordMapperProvider — пишем production-grade @Bean:

@Configuration
public class JooqConfig {

    @Bean
    public DefaultConfigurationCustomizer jooqConfigCustomizer(JooqJsonbHelper jsonbHelper) {
        return config -> config
            .set(SQLDialect.POSTGRES)
            .set(new Settings()
                .withExecuteLogging(false)
                .withRenderSchema(false))
            .set(customRecordMapperProvider(jsonbHelper));
    }

    private RecordMapperProvider customRecordMapperProvider(JooqJsonbHelper jsonbHelper) {
        return new DefaultRecordMapperProvider(/* ... */);
    }
}

Spring-стартер увидит DefaultConfigurationCustomizer и применит до создания DSLContext. После этого bean уходит в Spring scope, иммутабелен, переиспользуется всеми репозиториями.

Альтернатива «DSL.using(connection).settings(...) на месте» — это:

  • Новый Configuration (без reflection-кеша).
  • Игнорирование Spring-управляемого ConnectionProvider — может потечь соединение.
  • Невозможность переиспользовать настройки в других репозиториях.

Что запрещено

R-JOOQ-CTX-X1: DSL.using(connection, ...) или DSL.using(dataSource) в репозитории.

// ПЛОХО
public List<Order> findActive() {
    try (Connection conn = dataSource.getConnection()) {
        DSLContext ctx = DSL.using(conn, SQLDialect.POSTGRES);  // ← новый DSLContext на лету
        return ctx.selectFrom(ORDERS).where(...).fetchInto(Order.class);
    }
}

Что не так:

  • Соединение управляется руками — выходит из-под Spring transactional-context'а. Если этот метод вызван из @Transactional, jOOQ откроет второе соединение мимо Spring-транзакции, и UPDATE в той же транзакции пройдёт по разным connection'ам — гарантии нет.
  • Каждый вызов — новый Configuration без кешей. Под нагрузкой — заметная просадка.
  • Зачем вообще здесь try-with-resources, если Spring уже умеет управлять connection lifecycle? Это переписывание того, что фреймворк делает корректно.

Просто private final DSLContext dslContext через конструктор.

R-JOOQ-CTX-X2: хранение Connection или DSLContext в state репозитория вне Spring-проводки:

// ПЛОХО
@Repository
public class JooqOrderRepository {
    private Connection conn;       // ← где этот conn открывается, где закрывается?
    private DSLContext dslContext; // ← кто инициирует?
}

Если поля не private final и не инжектируются конструктором — Spring их не контролирует. Это путь к утечке соединений, race conditions, и невозможности тестировать репозиторий через DI.

Конструкторное внедрение через @RequiredArgsConstructor, final поля — единственная корректная схема (см. также R-JOOQ-REPO-2 в Repository pattern).

Куда дальше