Опирается на правила:
R-JOOQ-CTX-1…R-JOOQ-CTX-3иR-JOOQ-CTX-X1…R-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); пул сам поток-безопасен.SQLDialect—POSTGRES, статическая константа.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).
Куда дальше
- jOOQ Style Guide → раздел 3. DSLContext — нормативные формулировки.
- Repository pattern в jOOQ — где живёт
private final DSLContext. - Spring Bootstrap → BS-18 — про
spring-boot-starter-jooq.