From c2e86096cd6b7c184740ca110899f649a4598754 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 5 Jan 2022 15:46:54 +0000 Subject: [PATCH] Reinstate support for jOOQ as it now supports Jakarta EE 9 Closes gh-29271 --- .../spring-boot-autoconfigure/build.gradle | 3 + .../jooq/DefaultConfigurationCustomizer.java | 37 +++ .../DslContextDependsOnPostProcessor.java | 57 ++++ .../jooq/JooqAutoConfiguration.java | 154 ++++++++++ .../jooq/JooqExceptionTranslator.java | 97 +++++++ .../autoconfigure/jooq/JooqProperties.java | 61 ++++ .../jooq/NoDslContextBeanFailureAnalyzer.java | 69 +++++ .../autoconfigure/jooq/SpringTransaction.java | 44 +++ .../jooq/SpringTransactionProvider.java | 67 +++++ .../autoconfigure/jooq/SqlDialectLookup.java | 66 +++++ .../boot/autoconfigure/jooq/package-info.java | 20 ++ .../main/resources/META-INF/spring.factories | 2 + .../flyway/FlywayAutoConfigurationTests.java | 42 +++ .../jooq/JooqAutoConfigurationTests.java | 262 ++++++++++++++++++ .../jooq/JooqExceptionTranslatorTests.java | 90 ++++++ .../jooq/JooqPropertiesTests.java | 122 ++++++++ .../NoDslContextBeanFailureAnalyzerTests.java | 56 ++++ .../jooq/SqlDialectLookupTests.java | 105 +++++++ .../LiquibaseAutoConfigurationTests.java | 22 +- .../spring-boot-dependencies/build.gradle | 13 + .../spring-boot-docs/build.gradle | 3 + .../docs/asciidoc/anchor-rewrite.properties | 12 + .../src/docs/asciidoc/attributes.adoc | 1 + .../src/docs/asciidoc/data/sql.adoc | 84 ++++++ .../src/docs/asciidoc/features/logging.adoc | 2 +- .../src/docs/asciidoc/features/testing.adoc | 24 ++ .../src/docs/asciidoc/howto/data-access.adoc | 9 + .../asciidoc/howto/data-initialization.adoc | 1 + .../docs/data/sql/jooq/dslcontext/MyBean.java | 46 +++ .../docs/data/sql/jooq/dslcontext/Tables.java | 49 ++++ .../autoconfiguredjooq/MyJooqTests.java | 33 +++ .../spring-boot-starter-jooq/build.gradle | 11 + .../build.gradle | 3 + .../autoconfigure/jooq/AutoConfigureJooq.java | 43 +++ .../test/autoconfigure/jooq/JooqTest.java | 113 ++++++++ .../jooq/JooqTestContextBootstrapper.java | 37 +++ .../jooq/JooqTypeExcludeFilter.java | 34 +++ .../test/autoconfigure/jooq/package-info.java | 20 ++ .../jooq/ExampleJooqApplication.java | 40 +++ .../jooq/JooqTestIntegrationTests.java | 88 ++++++ .../JooqTestPropertiesIntegrationTests.java | 43 +++ ...ConfigureTestDatabaseIntegrationTests.java | 53 ++++ spring-boot-project/spring-boot/build.gradle | 3 + ...pendsOnDatabaseInitializationDetector.java | 39 +++ .../boot/jooq/package-info.java | 22 ++ .../main/resources/META-INF/spring.factories | 1 + 46 files changed, 2201 insertions(+), 2 deletions(-) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqProperties.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransaction.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransactionProvider.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/package-info.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookupTests.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/MyBean.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/Tables.java create mode 100644 spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java create mode 100644 spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/AutoConfigureJooq.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTest.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestContextBootstrapper.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTypeExcludeFilter.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/package-info.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/ExampleJooqApplication.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestPropertiesIntegrationTests.java create mode 100644 spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestWithAutoConfigureTestDatabaseIntegrationTests.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/JooqDependsOnDatabaseInitializationDetector.java create mode 100644 spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/package-info.java diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index b142648f926..b406372ef36 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -98,6 +98,9 @@ dependencies { } optional("org.hibernate.validator:hibernate-validator") optional("org.influxdb:influxdb-java") + optional("org.jooq:jooq") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } optional("org.liquibase:liquibase-core") { exclude group: "javax.xml.bind", module: "jaxb-api" } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java new file mode 100644 index 00000000000..78834633d75 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DefaultConfigurationCustomizer.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.impl.DefaultConfiguration; + +/** + * Callback interface that can be implemented by beans wishing to customize the + * {@link DefaultConfiguration} whilst retaining default auto-configuration. + * + * @author Stephane Nicoll + * @since 2.5.0 + */ +@FunctionalInterface +public interface DefaultConfigurationCustomizer { + + /** + * Customize the {@link DefaultConfiguration jOOQ Configuration}. + * @param configuration the configuration to customize + */ + void customize(DefaultConfiguration configuration); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java new file mode 100644 index 00000000000..f0667012a93 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/DslContextDependsOnPostProcessor.java @@ -0,0 +1,57 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.boot.autoconfigure.AbstractDependsOnBeanFactoryPostProcessor; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; + +/** + * {@link BeanFactoryPostProcessor} that can be used to dynamically declare that all + * {@link DSLContext} beans should "depend on" one or more specific beans. + * + * @author EddĂș MelĂ©ndez + * @since 2.3.9 + * @see BeanDefinition#setDependsOn(String[]) + * @deprecated since 2.5.0 for removal in 2.7.0 in favor of + * {@link DependsOnDatabaseInitializationDetector} + */ +@Deprecated +public class DslContextDependsOnPostProcessor extends AbstractDependsOnBeanFactoryPostProcessor { + + /** + * Creates a new {@code DslContextDependsOnPostProcessor} that will set up + * dependencies upon beans with the given names. + * @param dependsOn names of the beans to depend upon + */ + public DslContextDependsOnPostProcessor(String... dependsOn) { + super(DSLContext.class, dependsOn); + } + + /** + * Creates a new {@code DslContextDependsOnPostProcessor} that will set up + * dependencies upon beans with the given types. + * @param dependsOn types of the beans to depend upon + */ + public DslContextDependsOnPostProcessor(Class... dependsOn) { + super(DSLContext.class, dependsOn); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java new file mode 100644 index 00000000000..749ee10a27f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfiguration.java @@ -0,0 +1,154 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.jooq.ConnectionProvider; +import org.jooq.DSLContext; +import org.jooq.ExecuteListenerProvider; +import org.jooq.ExecutorProvider; +import org.jooq.RecordListenerProvider; +import org.jooq.RecordMapperProvider; +import org.jooq.RecordUnmapperProvider; +import org.jooq.TransactionListenerProvider; +import org.jooq.TransactionProvider; +import org.jooq.VisitListenerProvider; +import org.jooq.conf.Settings; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultConfiguration; +import org.jooq.impl.DefaultDSLContext; +import org.jooq.impl.DefaultExecuteListenerProvider; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.PlatformTransactionManager; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for JOOQ. + * + * @author Andreas Ahlenstorf + * @author Michael Simons + * @author Dmytro Nosan + * @since 1.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(DSLContext.class) +@ConditionalOnBean(DataSource.class) +@AutoConfigureAfter({ DataSourceAutoConfiguration.class, TransactionAutoConfiguration.class }) +public class JooqAutoConfiguration { + + @Bean + @ConditionalOnMissingBean(ConnectionProvider.class) + public DataSourceConnectionProvider dataSourceConnectionProvider(DataSource dataSource) { + return new DataSourceConnectionProvider(new TransactionAwareDataSourceProxy(dataSource)); + } + + @Bean + @ConditionalOnBean(PlatformTransactionManager.class) + public SpringTransactionProvider transactionProvider(PlatformTransactionManager txManager) { + return new SpringTransactionProvider(txManager); + } + + @Bean + @Order(0) + public DefaultExecuteListenerProvider jooqExceptionTranslatorExecuteListenerProvider() { + return new DefaultExecuteListenerProvider(new JooqExceptionTranslator()); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(DSLContext.class) + @EnableConfigurationProperties(JooqProperties.class) + public static class DslContextConfiguration { + + @Bean + public DefaultDSLContext dslContext(org.jooq.Configuration configuration) { + return new DefaultDSLContext(configuration); + } + + @Bean + @ConditionalOnMissingBean(org.jooq.Configuration.class) + public DefaultConfiguration jooqConfiguration(JooqProperties properties, ConnectionProvider connectionProvider, + DataSource dataSource, ObjectProvider executeListenerProviders, + ObjectProvider configurationCustomizers) { + DefaultConfiguration configuration = new DefaultConfiguration(); + configuration.set(properties.determineSqlDialect(dataSource)); + configuration.set(connectionProvider); + configuration.set(executeListenerProviders.orderedStream().toArray(ExecuteListenerProvider[]::new)); + configurationCustomizers.orderedStream().forEach((customizer) -> customizer.customize(configuration)); + return configuration; + } + + @Bean + @Deprecated + public DefaultConfigurationCustomizer jooqProvidersDefaultConfigurationCustomizer( + ObjectProvider transactionProvider, + ObjectProvider recordMapperProvider, + ObjectProvider recordUnmapperProvider, ObjectProvider settings, + ObjectProvider recordListenerProviders, + ObjectProvider visitListenerProviders, + ObjectProvider transactionListenerProviders, + ObjectProvider executorProvider) { + return new OrderedDefaultConfigurationCustomizer((configuration) -> { + transactionProvider.ifAvailable(configuration::set); + recordMapperProvider.ifAvailable(configuration::set); + recordUnmapperProvider.ifAvailable(configuration::set); + settings.ifAvailable(configuration::set); + executorProvider.ifAvailable(configuration::set); + configuration.set(recordListenerProviders.orderedStream().toArray(RecordListenerProvider[]::new)); + configuration.set(visitListenerProviders.orderedStream().toArray(VisitListenerProvider[]::new)); + configuration.setTransactionListenerProvider( + transactionListenerProviders.orderedStream().toArray(TransactionListenerProvider[]::new)); + }); + } + + } + + private static class OrderedDefaultConfigurationCustomizer implements DefaultConfigurationCustomizer, Ordered { + + private final DefaultConfigurationCustomizer delegate; + + OrderedDefaultConfigurationCustomizer(DefaultConfigurationCustomizer delegate) { + this.delegate = delegate; + } + + @Override + public void customize(DefaultConfiguration configuration) { + this.delegate.customize(configuration); + + } + + @Override + public int getOrder() { + return 0; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java new file mode 100644 index 00000000000..effe1859433 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslator.java @@ -0,0 +1,97 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import java.sql.SQLException; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DefaultExecuteListener; + +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator; +import org.springframework.jdbc.support.SQLExceptionTranslator; +import org.springframework.jdbc.support.SQLStateSQLExceptionTranslator; + +/** + * Transforms {@link java.sql.SQLException} into a Spring-specific + * {@link DataAccessException}. + * + * @author Lukas Eder + * @author Andreas Ahlenstorf + * @author Phillip Webb + * @author Stephane Nicoll + * @since 1.5.10 + */ +public class JooqExceptionTranslator extends DefaultExecuteListener { + + // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ + + private static final Log logger = LogFactory.getLog(JooqExceptionTranslator.class); + + @Override + public void exception(ExecuteContext context) { + SQLExceptionTranslator translator = getTranslator(context); + // The exception() callback is not only triggered for SQL exceptions but also for + // "normal" exceptions. In those cases sqlException() returns null. + SQLException exception = context.sqlException(); + while (exception != null) { + handle(context, translator, exception); + exception = exception.getNextException(); + } + } + + private SQLExceptionTranslator getTranslator(ExecuteContext context) { + SQLDialect dialect = context.configuration().dialect(); + if (dialect != null && dialect.thirdParty() != null) { + String dbName = dialect.thirdParty().springDbName(); + if (dbName != null) { + return new SQLErrorCodeSQLExceptionTranslator(dbName); + } + } + return new SQLStateSQLExceptionTranslator(); + } + + /** + * Handle a single exception in the chain. SQLExceptions might be nested multiple + * levels deep. The outermost exception is usually the least interesting one ("Call + * getNextException to see the cause."). Therefore the innermost exception is + * propagated and all other exceptions are logged. + * @param context the execute context + * @param translator the exception translator + * @param exception the exception + */ + private void handle(ExecuteContext context, SQLExceptionTranslator translator, SQLException exception) { + DataAccessException translated = translate(context, translator, exception); + if (exception.getNextException() == null) { + if (translated != null) { + context.exception(translated); + } + } + else { + logger.error("Execution of SQL statement failed.", (translated != null) ? translated : exception); + } + } + + private DataAccessException translate(ExecuteContext context, SQLExceptionTranslator translator, + SQLException exception) { + return translator.translate("jOOQ", context.sql(), exception); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqProperties.java new file mode 100644 index 00000000000..ccf73e8ea2f --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/JooqProperties.java @@ -0,0 +1,61 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.jooq.SQLDialect; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration properties for the JOOQ database library. + * + * @author Andreas Ahlenstorf + * @author Michael Simons + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.jooq") +public class JooqProperties { + + /** + * SQL dialect to use. Auto-detected by default. + */ + private SQLDialect sqlDialect; + + public SQLDialect getSqlDialect() { + return this.sqlDialect; + } + + public void setSqlDialect(SQLDialect sqlDialect) { + this.sqlDialect = sqlDialect; + } + + /** + * Determine the {@link SQLDialect} to use based on this configuration and the primary + * {@link DataSource}. + * @param dataSource the data source + * @return the {@code SQLDialect} to use for that {@link DataSource} + */ + public SQLDialect determineSqlDialect(DataSource dataSource) { + if (this.sqlDialect != null) { + return this.sqlDialect; + } + return SqlDialectLookup.getDialect(dataSource); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java new file mode 100644 index 00000000000..fa718df2b1e --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzer.java @@ -0,0 +1,69 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.diagnostics.AbstractFailureAnalyzer; +import org.springframework.boot.diagnostics.FailureAnalysis; +import org.springframework.core.Ordered; + +class NoDslContextBeanFailureAnalyzer extends AbstractFailureAnalyzer + implements Ordered, BeanFactoryAware { + + private BeanFactory beanFactory; + + @Override + protected FailureAnalysis analyze(Throwable rootFailure, NoSuchBeanDefinitionException cause) { + if (DSLContext.class.equals(cause.getBeanType()) && hasR2dbcAutoConfiguration()) { + return new FailureAnalysis( + "jOOQ has not been auto-configured as R2DBC has been auto-configured in favor of JDBC and jOOQ " + + "auto-configuration does not yet support R2DBC. ", + "To use jOOQ with JDBC, exclude R2dbcAutoConfiguration. To use jOOQ with R2DBC, define your own " + + "jOOQ configuration.", + cause); + } + return null; + } + + private boolean hasR2dbcAutoConfiguration() { + try { + this.beanFactory.getBean(R2dbcAutoConfiguration.class); + return true; + } + catch (Exception ex) { + return false; + } + } + + @Override + public int getOrder() { + return 0; + } + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransaction.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransaction.java new file mode 100644 index 00000000000..8ad4e14b4a3 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransaction.java @@ -0,0 +1,44 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.Transaction; + +import org.springframework.transaction.TransactionStatus; + +/** + * Adapts a Spring transaction for JOOQ. + * + * @author Lukas Eder + * @author Andreas Ahlenstorf + * @author Phillip Webb + */ +class SpringTransaction implements Transaction { + + // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ + + private final TransactionStatus transactionStatus; + + SpringTransaction(TransactionStatus transactionStatus) { + this.transactionStatus = transactionStatus; + } + + TransactionStatus getTxStatus() { + return this.transactionStatus; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransactionProvider.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransactionProvider.java new file mode 100644 index 00000000000..f5c51e60c18 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SpringTransactionProvider.java @@ -0,0 +1,67 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.TransactionContext; +import org.jooq.TransactionProvider; + +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.DefaultTransactionDefinition; + +/** + * Allows Spring Transaction to be used with JOOQ. + * + * @author Lukas Eder + * @author Andreas Ahlenstorf + * @author Phillip Webb + * @since 1.5.10 + */ +public class SpringTransactionProvider implements TransactionProvider { + + // Based on the jOOQ-spring-example from https://github.com/jOOQ/jOOQ + + private final PlatformTransactionManager transactionManager; + + public SpringTransactionProvider(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + } + + @Override + public void begin(TransactionContext context) { + TransactionDefinition definition = new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_NESTED); + TransactionStatus status = this.transactionManager.getTransaction(definition); + context.transaction(new SpringTransaction(status)); + } + + @Override + public void commit(TransactionContext ctx) { + this.transactionManager.commit(getTransactionStatus(ctx)); + } + + @Override + public void rollback(TransactionContext ctx) { + this.transactionManager.rollback(getTransactionStatus(ctx)); + } + + private TransactionStatus getTransactionStatus(TransactionContext ctx) { + SpringTransaction transaction = (SpringTransaction) ctx.transaction(); + return transaction.getTxStatus(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java new file mode 100644 index 00000000000..b91b72b6825 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookup.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import java.sql.DatabaseMetaData; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jooq.SQLDialect; +import org.jooq.tools.jdbc.JDBCUtils; + +import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.jdbc.support.MetaDataAccessException; + +/** + * Utility to lookup well known {@link SQLDialect SQLDialects} from a {@link DataSource}. + * + * @author Michael Simons + * @author Lukas Eder + */ +final class SqlDialectLookup { + + private static final Log logger = LogFactory.getLog(SqlDialectLookup.class); + + private SqlDialectLookup() { + } + + /** + * Return the most suitable {@link SQLDialect} for the given {@link DataSource}. + * @param dataSource the source {@link DataSource} + * @return the most suitable {@link SQLDialect} + */ + static SQLDialect getDialect(DataSource dataSource) { + if (dataSource == null) { + return SQLDialect.DEFAULT; + } + try { + String url = JdbcUtils.extractDatabaseMetaData(dataSource, DatabaseMetaData::getURL); + SQLDialect sqlDialect = JDBCUtils.dialect(url); + if (sqlDialect != null) { + return sqlDialect; + } + } + catch (MetaDataAccessException ex) { + logger.warn("Unable to determine jdbc url from datasource", ex); + } + return SQLDialect.DEFAULT; + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/package-info.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/package-info.java new file mode 100644 index 00000000000..8f455ab5bbb --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/jooq/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for JOOQ. + */ +package org.springframework.boot.autoconfigure.jooq; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 98b6ba194e2..367d9d890b1 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -87,6 +87,7 @@ org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConf org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\ org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\ org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\ +org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\ org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\ org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\ org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\ @@ -158,6 +159,7 @@ org.springframework.boot.autoconfigure.diagnostics.analyzer.NoSuchBeanDefinition org.springframework.boot.autoconfigure.flyway.FlywayMigrationScriptMissingFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.DataSourceBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.jdbc.HikariDriverConfigurationFailureAnalyzer,\ +org.springframework.boot.autoconfigure.jooq.NoDslContextBeanFailureAnalyzer,\ org.springframework.boot.autoconfigure.r2dbc.ConnectionFactoryBeanCreationFailureAnalyzer,\ org.springframework.boot.autoconfigure.r2dbc.MissingR2dbcPoolDependencyFailureAnalyzer,\ org.springframework.boot.autoconfigure.r2dbc.MultipleConnectionPoolConfigurationsFailureAnalzyer,\ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java index 334d0f87e8b..c075006c14c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/flyway/FlywayAutoConfigurationTests.java @@ -36,11 +36,15 @@ import org.flywaydb.core.internal.license.FlywayTeamsUpgradeRequiredException; import org.flywaydb.core.internal.plugin.PluginRegister; import org.flywaydb.database.sqlserver.SQLServerConfigurationExtension; import org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform; +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.jooq.impl.DefaultDSLContext; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InOrder; import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; @@ -649,6 +653,34 @@ class FlywayAutoConfigurationTests { .run(validateFlywayTeamsPropertyOnly("skipExecutingMigrations")); } + @Test + void whenFlywayIsAutoConfiguredThenJooqDslContextDependsOnFlywayBeans() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class) + .run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayInitializer", "flyway"); + }); + } + + @Test + void whenCustomMigrationInitializerIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, + CustomFlywayMigrationInitializer.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("flywayMigrationInitializer", + "flyway"); + }); + } + + @Test + void whenCustomFlywayIsDefinedThenJooqDslContextDependsOnIt() { + this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class, JooqConfiguration.class, + CustomFlyway.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactlyInAnyOrder("customFlyway"); + }); + } + @Test void baselineMigrationPrefixIsCorrectlyMapped() { this.contextRunner.withUserConfiguration(EmbeddedDataSourceConfiguration.class) @@ -920,6 +952,16 @@ class FlywayAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + static class JooqConfiguration { + + @Bean + DSLContext dslContext() { + return new DefaultDSLContext(SQLDialect.H2); + } + + } + @Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(DataSourceProperties.class) abstract static class AbstractUserH2DataSourceConfiguration { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java new file mode 100644 index 00000000000..bef9932c253 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqAutoConfigurationTests.java @@ -0,0 +1,262 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.jooq.CharsetProvider; +import org.jooq.ConnectionProvider; +import org.jooq.ConverterProvider; +import org.jooq.DSLContext; +import org.jooq.ExecuteListener; +import org.jooq.ExecuteListenerProvider; +import org.jooq.ExecutorProvider; +import org.jooq.RecordListenerProvider; +import org.jooq.RecordMapperProvider; +import org.jooq.RecordUnmapperProvider; +import org.jooq.SQLDialect; +import org.jooq.TransactionListenerProvider; +import org.jooq.TransactionalRunnable; +import org.jooq.VisitListenerProvider; +import org.jooq.impl.DataSourceConnectionProvider; +import org.jooq.impl.DefaultExecuteListenerProvider; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.jdbc.DataSourceBuilder; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link JooqAutoConfiguration}. + * + * @author Andreas Ahlenstorf + * @author Phillip Webb + * @author Andy Wilkinson + * @author Stephane Nicoll + * @author Dmytro Nosan + */ +class JooqAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) + .withPropertyValues("spring.datasource.name:jooqtest"); + + @Test + void noDataSource() { + this.contextRunner.run((context) -> assertThat(context.getBeansOfType(DSLContext.class)).isEmpty()); + } + + @Test + void jooqWithoutTx() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + assertThat(context).doesNotHaveBean(PlatformTransactionManager.class); + assertThat(context).doesNotHaveBean(SpringTransactionProvider.class); + DSLContext dsl = context.getBean(DSLContext.class); + dsl.execute("create table jooqtest (name varchar(255) primary key);"); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest;", "0")); + dsl.transaction(new ExecuteSql(dsl, "insert into jooqtest (name) values ('foo');")); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest;", "1")); + assertThatExceptionOfType(DataIntegrityViolationException.class) + .isThrownBy(() -> dsl.transaction(new ExecuteSql(dsl, "insert into jooqtest (name) values ('bar');", + "insert into jooqtest (name) values ('foo');"))); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest;", "2")); + }); + } + + @Test + void jooqWithTx() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TxManagerConfiguration.class) + .run((context) -> { + assertThat(context).hasSingleBean(PlatformTransactionManager.class); + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().dialect()).isEqualTo(SQLDialect.HSQLDB); + dsl.execute("create table jooqtest_tx (name varchar(255) primary key);"); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest_tx;", "0")); + dsl.transaction(new ExecuteSql(dsl, "insert into jooqtest_tx (name) values ('foo');")); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest_tx;", "1")); + assertThatExceptionOfType(DataIntegrityViolationException.class).isThrownBy( + () -> dsl.transaction(new ExecuteSql(dsl, "insert into jooqtest (name) values ('bar');", + "insert into jooqtest (name) values ('foo');"))); + dsl.transaction(new AssertFetch(dsl, "select count(*) as total from jooqtest_tx;", "1")); + }); + } + + @Test + void jooqWithDefaultConnectionProvider() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + ConnectionProvider connectionProvider = dsl.configuration().connectionProvider(); + assertThat(connectionProvider).isInstanceOf(DataSourceConnectionProvider.class); + DataSource connectionProviderDataSource = ((DataSourceConnectionProvider) connectionProvider).dataSource(); + assertThat(connectionProviderDataSource).isInstanceOf(TransactionAwareDataSourceProxy.class); + }); + } + + @Test + void jooqWithDefaultExecuteListenerProvider() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().executeListenerProviders()).hasSize(1); + }); + } + + @Test + void jooqWithSeveralExecuteListenerProviders() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TestExecuteListenerProvider.class) + .run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + ExecuteListenerProvider[] executeListenerProviders = dsl.configuration().executeListenerProviders(); + assertThat(executeListenerProviders).hasSize(2); + assertThat(executeListenerProviders[0]).isInstanceOf(DefaultExecuteListenerProvider.class); + assertThat(executeListenerProviders[1]).isInstanceOf(TestExecuteListenerProvider.class); + }); + } + + @Test + void dslContextWithConfigurationCustomizersAreApplied() { + ConverterProvider converterProvider = mock(ConverterProvider.class); + CharsetProvider charsetProvider = mock(CharsetProvider.class); + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class) + .withBean("configurationCustomizer1", DefaultConfigurationCustomizer.class, + () -> (configuration) -> configuration.set(converterProvider)) + .withBean("configurationCustomizer2", DefaultConfigurationCustomizer.class, + () -> (configuration) -> configuration.set(charsetProvider)) + .run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().converterProvider()).isSameAs(converterProvider); + assertThat(dsl.configuration().charsetProvider()).isSameAs(charsetProvider); + }); + } + + @Test + @Deprecated + void customProvidersArePickedUp() { + RecordMapperProvider recordMapperProvider = mock(RecordMapperProvider.class); + RecordUnmapperProvider recordUnmapperProvider = mock(RecordUnmapperProvider.class); + RecordListenerProvider recordListenerProvider = mock(RecordListenerProvider.class); + VisitListenerProvider visitListenerProvider = mock(VisitListenerProvider.class); + TransactionListenerProvider transactionListenerProvider = mock(TransactionListenerProvider.class); + ExecutorProvider executorProvider = mock(ExecutorProvider.class); + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class, TxManagerConfiguration.class) + .withBean(RecordMapperProvider.class, () -> recordMapperProvider) + .withBean(RecordUnmapperProvider.class, () -> recordUnmapperProvider) + .withBean(RecordListenerProvider.class, () -> recordListenerProvider) + .withBean(VisitListenerProvider.class, () -> visitListenerProvider) + .withBean(TransactionListenerProvider.class, () -> transactionListenerProvider) + .withBean(ExecutorProvider.class, () -> executorProvider).run((context) -> { + DSLContext dsl = context.getBean(DSLContext.class); + assertThat(dsl.configuration().recordMapperProvider()).isSameAs(recordMapperProvider); + assertThat(dsl.configuration().recordUnmapperProvider()).isSameAs(recordUnmapperProvider); + assertThat(dsl.configuration().executorProvider()).isSameAs(executorProvider); + assertThat(dsl.configuration().recordListenerProviders()).containsExactly(recordListenerProvider); + assertThat(dsl.configuration().visitListenerProviders()).containsExactly(visitListenerProvider); + assertThat(dsl.configuration().transactionListenerProviders()) + .containsExactly(transactionListenerProvider); + }); + } + + @Test + void relaxedBindingOfSqlDialect() { + this.contextRunner.withUserConfiguration(JooqDataSourceConfiguration.class) + .withPropertyValues("spring.jooq.sql-dialect:PoSTGrES") + .run((context) -> assertThat(context.getBean(org.jooq.Configuration.class).dialect()) + .isEqualTo(SQLDialect.POSTGRES)); + } + + static class AssertFetch implements TransactionalRunnable { + + private final DSLContext dsl; + + private final String sql; + + private final String expected; + + AssertFetch(DSLContext dsl, String sql, String expected) { + this.dsl = dsl; + this.sql = sql; + this.expected = expected; + } + + @Override + public void run(org.jooq.Configuration configuration) { + assertThat(this.dsl.fetch(this.sql).getValue(0, 0).toString()).isEqualTo(this.expected); + } + + } + + static class ExecuteSql implements TransactionalRunnable { + + private final DSLContext dsl; + + private final String[] sql; + + ExecuteSql(DSLContext dsl, String... sql) { + this.dsl = dsl; + this.sql = sql; + } + + @Override + public void run(org.jooq.Configuration configuration) { + for (String statement : this.sql) { + this.dsl.execute(statement); + } + } + + } + + @Configuration(proxyBeanMethods = false) + static class JooqDataSourceConfiguration { + + @Bean + DataSource jooqDataSource() { + return DataSourceBuilder.create().url("jdbc:hsqldb:mem:jooqtest").username("sa").build(); + } + + } + + @Configuration(proxyBeanMethods = false) + static class TxManagerConfiguration { + + @Bean + PlatformTransactionManager transactionManager(DataSource dataSource) { + return new DataSourceTransactionManager(dataSource); + } + + } + + @Order(100) + static class TestExecuteListenerProvider implements ExecuteListenerProvider { + + @Override + public ExecuteListener provide() { + return null; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java new file mode 100644 index 00000000000..581cd512efe --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqExceptionTranslatorTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import java.sql.SQLException; + +import org.jooq.Configuration; +import org.jooq.ExecuteContext; +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; + +import org.springframework.jdbc.BadSqlGrammarException; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JooqExceptionTranslator} + * + * @author Andy Wilkinson + */ +class JooqExceptionTranslatorTests { + + private final JooqExceptionTranslator exceptionTranslator = new JooqExceptionTranslator(); + + @ParameterizedTest(name = "{0}") + @MethodSource + void exceptionTranslation(SQLDialect dialect, SQLException sqlException) { + ExecuteContext context = mock(ExecuteContext.class); + Configuration configuration = mock(Configuration.class); + given(context.configuration()).willReturn(configuration); + given(configuration.dialect()).willReturn(dialect); + given(context.sqlException()).willReturn(sqlException); + this.exceptionTranslator.exception(context); + ArgumentCaptor captor = ArgumentCaptor.forClass(RuntimeException.class); + verify(context).exception(captor.capture()); + assertThat(captor.getValue()).isInstanceOf(BadSqlGrammarException.class); + } + + @Test + void whenExceptionCannotBeTranslatedThenExecuteContextExceptionIsNotCalled() { + ExecuteContext context = mock(ExecuteContext.class); + Configuration configuration = mock(Configuration.class); + given(context.configuration()).willReturn(configuration); + given(configuration.dialect()).willReturn(SQLDialect.POSTGRES); + given(context.sqlException()).willReturn(new SQLException(null, null, 123456789)); + this.exceptionTranslator.exception(context); + verify(context, times(0)).exception(any()); + } + + static Object[] exceptionTranslation() { + return new Object[] { new Object[] { SQLDialect.DERBY, sqlException("42802") }, + new Object[] { SQLDialect.H2, sqlException(42000) }, + new Object[] { SQLDialect.HSQLDB, sqlException(-22) }, + new Object[] { SQLDialect.MARIADB, sqlException(1054) }, + new Object[] { SQLDialect.MYSQL, sqlException(1054) }, + new Object[] { SQLDialect.POSTGRES, sqlException("03000") }, + new Object[] { SQLDialect.SQLITE, sqlException("21000") } }; + } + + private static SQLException sqlException(String sqlState) { + return new SQLException(null, sqlState); + } + + private static SQLException sqlException(int vendorCode) { + return new SQLException(null, null, vendorCode); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java new file mode 100644 index 00000000000..3aa95104694 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/JooqPropertiesTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.jooq.SQLDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link JooqProperties}. + * + * @author Stephane Nicoll + */ +class JooqPropertiesTests { + + private AnnotationConfigApplicationContext context; + + @AfterEach + void close() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + void determineSqlDialectNoCheckIfDialectIsSet() throws SQLException { + JooqProperties properties = load("spring.jooq.sql-dialect=postgres"); + DataSource dataSource = mockStandaloneDataSource(); + SQLDialect sqlDialect = properties.determineSqlDialect(dataSource); + assertThat(sqlDialect).isEqualTo(SQLDialect.POSTGRES); + verify(dataSource, never()).getConnection(); + } + + @Test + void determineSqlDialectWithKnownUrl() { + JooqProperties properties = load(); + SQLDialect sqlDialect = properties.determineSqlDialect(mockDataSource("jdbc:h2:mem:testdb")); + assertThat(sqlDialect).isEqualTo(SQLDialect.H2); + } + + @Test + void determineSqlDialectWithKnownUrlAndUserConfig() { + JooqProperties properties = load("spring.jooq.sql-dialect=mysql"); + SQLDialect sqlDialect = properties.determineSqlDialect(mockDataSource("jdbc:h2:mem:testdb")); + assertThat(sqlDialect).isEqualTo(SQLDialect.MYSQL); + } + + @Test + void determineSqlDialectWithUnknownUrl() { + JooqProperties properties = load(); + SQLDialect sqlDialect = properties.determineSqlDialect(mockDataSource("jdbc:unknown://localhost")); + assertThat(sqlDialect).isEqualTo(SQLDialect.DEFAULT); + } + + private DataSource mockStandaloneDataSource() throws SQLException { + DataSource ds = mock(DataSource.class); + given(ds.getConnection()).willThrow(SQLException.class); + return ds; + } + + private DataSource mockDataSource(String jdbcUrl) { + DataSource ds = mock(DataSource.class); + try { + DatabaseMetaData metadata = mock(DatabaseMetaData.class); + given(metadata.getURL()).willReturn(jdbcUrl); + Connection connection = mock(Connection.class); + given(connection.getMetaData()).willReturn(metadata); + given(ds.getConnection()).willReturn(connection); + } + catch (SQLException ex) { + // Do nothing + } + return ds; + } + + private JooqProperties load(String... environment) { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + TestPropertyValues.of(environment).applyTo(ctx); + ctx.register(TestConfiguration.class); + ctx.refresh(); + this.context = ctx; + return this.context.getBean(JooqProperties.class); + } + + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(JooqProperties.class) + static class TestConfiguration { + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java new file mode 100644 index 00000000000..c35aa5a1409 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/NoDslContextBeanFailureAnalyzerTests.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import org.jooq.DSLContext; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link NoDslContextBeanFailureAnalyzer}. + * + * @author Andy Wilkinson + */ +class NoDslContextBeanFailureAnalyzerTests { + + private final NoDslContextBeanFailureAnalyzer failureAnalyzer = new NoDslContextBeanFailureAnalyzer(); + + @Test + void noAnalysisWithoutR2dbcAutoConfiguration() { + new ApplicationContextRunner().run((context) -> { + this.failureAnalyzer.setBeanFactory(context.getBeanFactory()); + assertThat(this.failureAnalyzer.analyze(new NoSuchBeanDefinitionException(DSLContext.class))).isNull(); + }); + } + + @Test + void analysisWithR2dbcAutoConfiguration() { + new ApplicationContextRunner().withConfiguration(AutoConfigurations.of(R2dbcAutoConfiguration.class)) + .run((context) -> { + this.failureAnalyzer.setBeanFactory(context.getBeanFactory()); + assertThat(this.failureAnalyzer.analyze(new NoSuchBeanDefinitionException(DSLContext.class))) + .isNotNull(); + }); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookupTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookupTests.java new file mode 100644 index 00000000000..e417a2e1eed --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/jooq/SqlDialectLookupTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.autoconfigure.jooq; + +import java.sql.Connection; +import java.sql.DatabaseMetaData; + +import javax.sql.DataSource; + +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link SqlDialectLookup}. + * + * @author Michael Simons + * @author Stephane Nicoll + */ +class SqlDialectLookupTests { + + @Test + void getSqlDialectWhenDataSourceIsNullShouldReturnDefault() { + assertThat(SqlDialectLookup.getDialect(null)).isEqualTo(SQLDialect.DEFAULT); + } + + @Test + void getSqlDialectWhenDataSourceIsUnknownShouldReturnDefault() throws Exception { + testGetSqlDialect("jdbc:idontexist:", SQLDialect.DEFAULT); + } + + @Test + void getSqlDialectWhenDerbyShouldReturnDerby() throws Exception { + testGetSqlDialect("jdbc:derby:", SQLDialect.DERBY); + } + + @Test + void getSqlDialectWhenH2ShouldReturnH2() throws Exception { + testGetSqlDialect("jdbc:h2:", SQLDialect.H2); + } + + @Test + void getSqlDialectWhenHsqldbShouldReturnHsqldb() throws Exception { + testGetSqlDialect("jdbc:hsqldb:", SQLDialect.HSQLDB); + } + + @Test + void getSqlDialectWhenMysqlShouldReturnMysql() throws Exception { + testGetSqlDialect("jdbc:mysql:", SQLDialect.MYSQL); + } + + @Test + void getSqlDialectWhenOracleShouldReturnDefault() throws Exception { + testGetSqlDialect("jdbc:oracle:", SQLDialect.DEFAULT); + } + + @Test + void getSqlDialectWhenPostgresShouldReturnPostgres() throws Exception { + testGetSqlDialect("jdbc:postgresql:", SQLDialect.POSTGRES); + } + + @Test + void getSqlDialectWhenSqlserverShouldReturnDefault() throws Exception { + testGetSqlDialect("jdbc:sqlserver:", SQLDialect.DEFAULT); + } + + @Test + void getSqlDialectWhenDb2ShouldReturnDefault() throws Exception { + testGetSqlDialect("jdbc:db2:", SQLDialect.DEFAULT); + } + + @Test + void getSqlDialectWhenInformixShouldReturnDefault() throws Exception { + testGetSqlDialect("jdbc:informix-sqli:", SQLDialect.DEFAULT); + } + + private void testGetSqlDialect(String url, SQLDialect expected) throws Exception { + DataSource dataSource = mock(DataSource.class); + Connection connection = mock(Connection.class); + DatabaseMetaData metaData = mock(DatabaseMetaData.class); + given(dataSource.getConnection()).willReturn(connection); + given(connection.getMetaData()).willReturn(metaData); + given(metaData.getURL()).willReturn(url); + SQLDialect sqlDialect = SqlDialectLookup.getDialect(dataSource); + assertThat(sqlDialect).isEqualTo(expected); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java index f98cc642442..d97c5819c1e 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/liquibase/LiquibaseAutoConfigurationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,7 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration; import org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration; import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.test.context.FilteredClassLoader; @@ -383,6 +384,25 @@ class LiquibaseAutoConfigurationTests { .run(assertLiquibase((liquibase) -> assertThat(liquibase.getTag()).isEqualTo("1.0.0"))); } + @Test + void whenLiquibaseIsAutoConfiguredThenJooqDslContextDependsOnSpringLiquibaseBeans() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) + .withUserConfiguration(EmbeddedDataSourceConfiguration.class).run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactly("liquibase"); + }); + } + + @Test + void whenCustomSpringLiquibaseIsDefinedThenJooqDslContextDependsOnSpringLiquibaseBeans() { + this.contextRunner.withConfiguration(AutoConfigurations.of(JooqAutoConfiguration.class)) + .withUserConfiguration(LiquibaseUserConfiguration.class, EmbeddedDataSourceConfiguration.class) + .run((context) -> { + BeanDefinition beanDefinition = context.getBeanFactory().getBeanDefinition("dslContext"); + assertThat(beanDefinition.getDependsOn()).containsExactly("springLiquibase"); + }); + } + private ContextConsumer assertLiquibase(Consumer consumer) { return (context) -> { assertThat(context).hasSingleBean(SpringLiquibase.class); diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 96ed21c67db..a841e23c6f7 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -652,6 +652,19 @@ bom { ] } } + library("jOOQ", "3.16.0") { + group("org.jooq") { + modules = [ + "jooq", + "jooq-codegen", + "jooq-kotlin", + "jooq-meta" + ] + plugins = [ + "jooq-codegen-maven" + ] + } + } library("Json Path", "2.6.0") { group("com.jayway.jsonpath") { modules = [ diff --git a/spring-boot-project/spring-boot-docs/build.gradle b/spring-boot-project/spring-boot-docs/build.gradle index 51327a914f0..fc5d46606be 100644 --- a/spring-boot-project/spring-boot-docs/build.gradle +++ b/spring-boot-project/spring-boot-docs/build.gradle @@ -102,6 +102,9 @@ dependencies { exclude group: "javax.xml.bind", module: "jaxb-api" exclude group: "org.jboss.spec.javax.transaction", module: "jboss-transaction-api_1.2_spec" } + implementation("org.jooq:jooq") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } implementation("org.mockito:mockito-core") implementation("org.mongodb:mongodb-driver-sync") implementation("org.quartz-scheduler:quartz") diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties index bfef0fdf824..8c0d2552797 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/anchor-rewrite.properties @@ -230,6 +230,11 @@ boot-features-spring-data-jpa-repositories=features.sql.jpa-and-spring-data.repo boot-features-creating-and-dropping-jpa-databases=features.sql.jpa-and-spring-data.creating-and-dropping boot-features-jpa-in-web-environment=features.sql.jpa-and-spring-data.open-entity-manager-in-view boot-features-data-jdbc=features.sql.jdbc +boot-features-jooq=features.sql.jooq +boot-features-jooq-codegen=features.sql.jooq.codegen +boot-features-jooq-dslcontext=features.sql.jooq.dslcontext +boot-features-jooq-sqldialect=features.sql.jooq.sqldialect +boot-features-jooq-customizing=features.sql.jooq.customizing boot-features-r2dbc=features.sql.r2dbc boot-features-r2dbc-embedded-database=features.sql.r2dbc.embedded boot-features-r2dbc-using-database-client=features.sql.r2dbc.using-database-client @@ -333,6 +338,7 @@ boot-features-testing-spring-boot-applications-testing-autoconfigured-cassandra- boot-features-testing-spring-boot-applications-testing-autoconfigured-jpa-test=features.testing.spring-boot-applications.autoconfigured-spring-data-jpa boot-features-testing-spring-boot-applications-testing-autoconfigured-jdbc-test=features.testing.spring-boot-applications.autoconfigured-jdbc boot-features-testing-spring-boot-applications-testing-autoconfigured-data-jdbc-test=features.testing.spring-boot-applications.autoconfigured-spring-data-jdbc +boot-features-testing-spring-boot-applications-testing-autoconfigured-jooq-test=features.testing.spring-boot-applications.autoconfigured-jooq boot-features-testing-spring-boot-applications-testing-autoconfigured-mongo-test=features.testing.spring-boot-applications.autoconfigured-spring-data-mongodb boot-features-testing-spring-boot-applications-testing-autoconfigured-neo4j-test=features.testing.spring-boot-applications.autoconfigured-spring-data-neo4j boot-features-testing-spring-boot-applications-testing-autoconfigured-redis-test=features.testing.spring-boot-applications.autoconfigured-spring-data-redis @@ -637,6 +643,7 @@ howto-use-spring-data-jpa--and-mongo-repositories=howto.data-access.use-spring-d howto-use-customize-spring-datas-web-support=howto.data-access.customize-spring-data-web-support howto-use-exposing-spring-data-repositories-rest-endpoint=howto.data-access.exposing-spring-data-repositories-as-rest howto-configure-a-component-that-is-used-by-JPA=howto.data-access.configure-a-component-that-is-used-by-jpa +howto-configure-jOOQ-with-multiple-datasources=howto.data-access.configure-jooq-with-multiple-datasources howto-database-initialization=howto.data-initialization howto-initialize-a-database-using-jpa=howto.data-initialization.using-jpa howto-initialize-a-database-using-hibernate=howto.data-initialization.using-hibernate @@ -909,6 +916,11 @@ features.sql.jpa-and-spring-data.open-entity-manager-in-view=data.sql.jpa-and-sp features.sql.jdbc=data.sql.jdbc features.sql.h2-web-console=data.sql.h2-web-console features.sql.h2-web-console.custom-path=data.sql.h2-web-console.custom-path +features.sql.jooq=data.sql.jooq +features.sql.jooq.codegen=data.sql.jooq.codegen +features.sql.jooq.dslcontext=data.sql.jooq.dslcontext +features.sql.jooq.sqldialect=data.sql.jooq.sqldialect +features.sql.jooq.customizing=data.sql.jooq.customizing features.sql.r2dbc=data.sql.r2dbc features.sql.r2dbc.embedded=data.sql.r2dbc.embedded features.sql.r2dbc.using-database-client=data.sql.r2dbc.using-database-client diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc index 709bfa018bc..3ba8afc70cc 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/attributes.adoc @@ -98,6 +98,7 @@ :gradle-docs: https://docs.gradle.org/current/userguide :hibernate-docs: https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html :java-api: https://docs.oracle.com/javase/8/docs/api +:jooq-docs: https://www.jooq.org/doc/{jooq-version}/manual-single-page :junit5-docs: https://junit.org/junit5/docs/current/user-guide :kotlin-docs: https://kotlinlang.org/docs/reference/ :lettuce-docs: https://lettuce.io/core/{lettuce-version}/reference/index.html diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc index 6612fae191e..dfe40bbda93 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/sql.adoc @@ -315,6 +315,90 @@ TIP: For complete details of Spring Data JDBC, see the {spring-data-jdbc-docs}[r +[[data.sql.jooq]] +=== Using jOOQ +jOOQ Object Oriented Querying (https://www.jooq.org/[jOOQ]) is a popular product from https://www.datageekery.com/[Data Geekery] which generates Java code from your database and lets you build type-safe SQL queries through its fluent API. +Both the commercial and open source editions can be used with Spring Boot. + + + +[[data.sql.jooq.codegen]] +==== Code Generation +In order to use jOOQ type-safe queries, you need to generate Java classes from your database schema. +You can follow the instructions in the {jooq-docs}/#jooq-in-7-steps-step3[jOOQ user manual]. +If you use the `jooq-codegen-maven` plugin and you also use the `spring-boot-starter-parent` "`parent POM`", you can safely omit the plugin's `` tag. +You can also use Spring Boot-defined version variables (such as `h2.version`) to declare the plugin's database dependency. +The following listing shows an example: + +[source,xml,indent=0,subs="verbatim"] +---- + + org.jooq + jooq-codegen-maven + + ... + + + + com.h2database + h2 + ${h2.version} + + + + + org.h2.Driver + jdbc:h2:~/yourdatabase + + + ... + + + +---- + + + +[[data.sql.jooq.dslcontext]] +==== Using DSLContext +The fluent API offered by jOOQ is initiated through the `org.jooq.DSLContext` interface. +Spring Boot auto-configures a `DSLContext` as a Spring Bean and connects it to your application `DataSource`. +To use the `DSLContext`, you can inject it, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/data/sql/jooq/dslcontext/MyBean.java[tag=!method] +---- + +TIP: The jOOQ manual tends to use a variable named `create` to hold the `DSLContext`. + +You can then use the `DSLContext` to construct your queries, as shown in the following example: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/data/sql/jooq/dslcontext/MyBean.java[tag=method] +---- + + + +[[data.sql.jooq.sqldialect]] +==== jOOQ SQL Dialect +Unless the configprop:spring.jooq.sql-dialect[] property has been configured, Spring Boot determines the SQL dialect to use for your datasource. +If Spring Boot could not detect the dialect, it uses `DEFAULT`. + +NOTE: Spring Boot can only auto-configure dialects supported by the open source version of jOOQ. + + + +[[data.sql.jooq.customizing]] +==== Customizing jOOQ +More advanced customizations can be achieved by defining your own `DefaultConfigurationCustomizer` bean that will be invoked prior to creating the `org.jooq.Configuration` `@Bean`. +This takes precedence to anything that is applied by the auto-configuration. + +You can also create your own `org.jooq.Configuration` `@Bean` if you want to take complete control of the jOOQ configuration. + + + [[data.sql.r2dbc]] === Using R2DBC The Reactive Relational Database Connectivity (https://r2dbc.io[R2DBC]) project brings reactive programming APIs to relational databases. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc index 77b364e0f90..626e1bdd300 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/logging.adoc @@ -255,7 +255,7 @@ Spring Boot includes the following pre-defined logging groups that can be used o | `org.springframework.core.codec`, `org.springframework.http`, `org.springframework.web`, `org.springframework.boot.actuate.endpoint.web`, `org.springframework.boot.web.servlet.ServletContextInitializerBeans` | sql -| `org.springframework.jdbc.core`, `org.hibernate.SQL` +| `org.springframework.jdbc.core`, `org.hibernate.SQL`, `org.jooq.tools.LoggerListener` |=== diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc index 97bd79710b3..4d8e5636806 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/features/testing.adoc @@ -553,6 +553,30 @@ If you prefer your test to run against a real database, you can use the `@AutoCo +[[features.testing.spring-boot-applications.autoconfigured-jooq]] +==== Auto-configured jOOQ Tests +You can use `@JooqTest` in a similar fashion as `@JdbcTest` but for jOOQ-related tests. +As jOOQ relies heavily on a Java-based schema that corresponds with the database schema, the existing `DataSource` is used. +If you want to replace it with an in-memory database, you can use `@AutoConfigureTestDatabase` to override those settings. +(For more about using jOOQ with Spring Boot, see "<>", earlier in this chapter.) +Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@JooqTest` annotation is used. +`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans. + +TIP: A list of the auto-configurations that are enabled by `@JooqTest` can be <>. + +`@JooqTest` configures a `DSLContext`. +The following example shows the `@JooqTest` annotation in use: + +[source,java,indent=0,subs="verbatim"] +---- +include::{docs-java}/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java[] +---- + +JOOQ tests are transactional and roll back at the end of each test by default. +If that is not what you want, you can disable transaction management for a test or for the whole test class as <>. + + + [[features.testing.spring-boot-applications.autoconfigured-spring-data-mongodb]] ==== Auto-configured Data MongoDB Tests You can use `@DataMongoTest` to test MongoDB applications. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc index a3492bc0e28..aba53368846 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-access.adoc @@ -394,3 +394,12 @@ For example, if you use Hibernate Search with Elasticsearch as its index manager ---- include::{docs-java}/howto/dataaccess/configureacomponentthatisusedbyjpa/ElasticsearchEntityManagerFactoryDependsOnPostProcessor.java[] ---- + + + +[[howto.data-access.configure-jooq-with-multiple-datasources]] +=== Configure jOOQ with Two DataSources +If you need to use jOOQ with multiple data sources, you should create your own `DSLContext` for each one. +See {spring-boot-autoconfigure-module-code}/jooq/JooqAutoConfiguration.java[JooqAutoConfiguration] for more details. + +TIP: In particular, `JooqExceptionTranslator` and `SpringTransactionProvider` can be reused to provide similar features to what the auto-configuration does with a single `DataSource`. diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc index e73742d7a6a..30b83f1189f 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/howto/data-initialization.adoc @@ -209,6 +209,7 @@ To have other beans be detected, register an implementation of `DatabaseInitiali Spring Boot will automatically detect beans of the following types that depends upon database initialization: - `AbstractEntityManagerFactoryBean` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) +- `DSLContext` (jOOQ) - `EntityManagerFactory` (unless configprop:spring.jpa.defer-datasource-initialization[] is set to `true`) - `JdbcOperations` - `NamedParameterJdbcOperations` diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/MyBean.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/MyBean.java new file mode 100644 index 00000000000..ab1ebed4810 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/MyBean.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.data.sql.jooq.dslcontext; + +import java.util.GregorianCalendar; +import java.util.List; + +import org.jooq.DSLContext; + +import org.springframework.stereotype.Component; + +import static org.springframework.boot.docs.data.sql.jooq.dslcontext.Tables.AUTHOR; + +@Component +public class MyBean { + + private final DSLContext create; + + public MyBean(DSLContext dslContext) { + this.create = dslContext; + } + + // tag::method[] + public List authorsBornAfter1980() { + // @formatter:off + return this.create.selectFrom(AUTHOR) + .where(AUTHOR.DATE_OF_BIRTH.greaterThan(new GregorianCalendar(1980, 0, 1))) + .fetch(AUTHOR.DATE_OF_BIRTH); + // @formatter:on + } // end::method[] + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/Tables.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/Tables.java new file mode 100644 index 00000000000..94a05966f42 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/data/sql/jooq/dslcontext/Tables.java @@ -0,0 +1,49 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.data.sql.jooq.dslcontext; + +import java.util.GregorianCalendar; + +import org.jooq.Name; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.impl.TableImpl; +import org.jooq.impl.TableRecordImpl; + +abstract class Tables { + + static final TAuthor AUTHOR = null; + + abstract class TAuthor extends TableImpl { + + TAuthor(Name name) { + super(name); + } + + public final TableField DATE_OF_BIRTH = null; + + } + + abstract class TAuthorRecord extends TableRecordImpl { + + TAuthorRecord(Table table) { + super(table); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java new file mode 100644 index 00000000000..16e631e17a6 --- /dev/null +++ b/spring-boot-project/spring-boot-docs/src/main/java/org/springframework/boot/docs/features/testing/springbootapplications/autoconfiguredjooq/MyJooqTests.java @@ -0,0 +1,33 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.docs.features.testing.springbootapplications.autoconfiguredjooq; + +import org.jooq.DSLContext; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jooq.JooqTest; + +@JooqTest +class MyJooqTests { + + @Autowired + @SuppressWarnings("unused") + private DSLContext dslContext; + + // ... + +} diff --git a/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle b/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle new file mode 100644 index 00000000000..aa4eda59433 --- /dev/null +++ b/spring-boot-project/spring-boot-starters/spring-boot-starter-jooq/build.gradle @@ -0,0 +1,11 @@ +plugins { + id "org.springframework.boot.starter" +} + +description = "Starter for using jOOQ to access SQL databases with JDBC. An alternative to spring-boot-starter-data-jpa or spring-boot-starter-jdbc" + +dependencies { + api(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-jdbc")) + api("org.springframework:spring-tx") + api("org.jooq:jooq") +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle index 454c62176b7..38f06491fa4 100644 --- a/spring-boot-project/spring-boot-test-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-test-autoconfigure/build.gradle @@ -78,6 +78,9 @@ dependencies { testImplementation("org.eclipse:yasson") testImplementation("org.hibernate.validator:hibernate-validator") testImplementation("org.hsqldb:hsqldb") + testImplementation("org.jooq:jooq") { + exclude group: "javax.xml.bind", module: "jaxb-api" + } testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.platform:junit-platform-engine") testImplementation("org.junit.platform:junit-platform-launcher") diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/AutoConfigureJooq.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/AutoConfigureJooq.java new file mode 100644 index 00000000000..e166144e0a6 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/AutoConfigureJooq.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; + +/** + * {@link ImportAutoConfiguration Auto-configuration imports} for typical jOOQ tests. Most + * tests should consider using {@link JooqTest @JooqTest} rather than using this + * annotation directly. + * + * @author Michael Simons + * @since 2.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@ImportAutoConfiguration +public @interface AutoConfigureJooq { + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTest.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTest.java new file mode 100644 index 00000000000..76270aa3534 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTest.java @@ -0,0 +1,113 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration; +import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache; +import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.context.annotation.ComponentScan.Filter; +import org.springframework.core.annotation.AliasFor; +import org.springframework.core.env.Environment; +import org.springframework.test.context.BootstrapWith; +import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.transaction.annotation.Transactional; + +/** + * Annotation for a jOOQ test that focuses only on jOOQ-based components. + *

+ * Using this annotation will disable full auto-configuration and instead apply only + * configuration relevant to jOOQ tests. + *

+ * By default, tests annotated with {@code @JooqTest} use the configured database. If you + * want to replace any explicit or usually auto-configured DataSource by an embedded + * in-memory database, the {@link AutoConfigureTestDatabase @AutoConfigureTestDatabase} + * annotation can be used to override these settings. + *

+ * When using JUnit 4, this annotation should be used in combination with + * {@code @RunWith(SpringRunner.class)}. + * + * @author Michael Simons + * @author Stephane Nicoll + * @author Artsiom Yudovin + * @since 2.0.0 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Inherited +@BootstrapWith(JooqTestContextBootstrapper.class) +@ExtendWith(SpringExtension.class) +@OverrideAutoConfiguration(enabled = false) +@TypeExcludeFilters(JooqTypeExcludeFilter.class) +@Transactional +@AutoConfigureCache +@AutoConfigureJooq +@ImportAutoConfiguration +public @interface JooqTest { + + /** + * Properties in form {@literal key=value} that should be added to the Spring + * {@link Environment} before the test runs. + * @return the properties to add + * @since 2.1.0 + */ + String[] properties() default {}; + + /** + * Determines if default filtering should be used with + * {@link SpringBootApplication @SpringBootApplication}. By default no beans are + * included. + * @see #includeFilters() + * @see #excludeFilters() + * @return if default filters should be used + */ + boolean useDefaultFilters() default true; + + /** + * A set of include filters which can be used to add otherwise filtered beans to the + * application context. + * @return include filters to apply + */ + Filter[] includeFilters() default {}; + + /** + * A set of exclude filters which can be used to filter beans that would otherwise be + * added to the application context. + * @return exclude filters to apply + */ + Filter[] excludeFilters() default {}; + + /** + * Auto-configuration exclusions that should be applied for this test. + * @return auto-configuration exclusions to apply + */ + @AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude") + Class[] excludeAutoConfiguration() default {}; + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestContextBootstrapper.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestContextBootstrapper.java new file mode 100644 index 00000000000..d2be5ad9bd5 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestContextBootstrapper.java @@ -0,0 +1,37 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import org.springframework.boot.test.context.SpringBootTestContextBootstrapper; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; +import org.springframework.test.context.TestContextBootstrapper; + +/** + * {@link TestContextBootstrapper} for {@link JooqTest @JooqTest} support. + * + * @author Artsiom Yudovin + */ +class JooqTestContextBootstrapper extends SpringBootTestContextBootstrapper { + + @Override + protected String[] getProperties(Class testClass) { + return MergedAnnotations.from(testClass, SearchStrategy.INHERITED_ANNOTATIONS).get(JooqTest.class) + .getValue("properties", String[].class).orElse(null); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTypeExcludeFilter.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTypeExcludeFilter.java new file mode 100644 index 00000000000..3aeeb3e5a72 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/JooqTypeExcludeFilter.java @@ -0,0 +1,34 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter; + +/** + * {@link TypeExcludeFilter} for {@link JooqTest @JooqTest}. + * + * @author Michael Simons + * @since 2.2.1 + */ +public final class JooqTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter { + + JooqTypeExcludeFilter(Class testClass) { + super(testClass); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/package-info.java b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/package-info.java new file mode 100644 index 00000000000..3fc5c3e5ecf --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/main/java/org/springframework/boot/test/autoconfigure/jooq/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Auto-configuration for jOOQ tests. + */ +package org.springframework.boot.test.autoconfigure.jooq; diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/ExampleJooqApplication.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/ExampleJooqApplication.java new file mode 100644 index 00000000000..00e3a922b24 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/ExampleJooqApplication.java @@ -0,0 +1,40 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; + +/** + * Example {@link SpringBootApplication @SpringBootApplication} used with + * {@link JooqTest @JooqTest} tests. + * + * @author Michael Simons + */ +@SpringBootApplication +public class ExampleJooqApplication { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.HSQL).build(); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestIntegrationTests.java new file mode 100644 index 00000000000..8a34d70c071 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestIntegrationTests.java @@ -0,0 +1,88 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration; +import org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration; +import org.springframework.boot.test.autoconfigure.orm.jpa.ExampleComponent; +import org.springframework.context.ApplicationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.boot.test.autoconfigure.AutoConfigurationImportedCondition.importedAutoConfiguration; + +/** + * Integration tests for {@link JooqTest @JooqTest}. + * + * @author Michael Simons + */ +@JooqTest +class JooqTestIntegrationTests { + + @Autowired + private DSLContext dsl; + + @Autowired + private DataSource dataSource; + + @Autowired + private ApplicationContext applicationContext; + + @Test + void testDSLContext() { + assertThat(this.dsl.selectCount().from("INFORMATION_SCHEMA.TABLES").fetchOne(0, Integer.class)) + .isGreaterThan(0); + } + + @Test + void useDefinedDataSource() throws Exception { + String product = this.dataSource.getConnection().getMetaData().getDatabaseProductName(); + assertThat(product).startsWith("HSQL"); + assertThat(this.dsl.configuration().dialect()).isEqualTo(SQLDialect.HSQLDB); + } + + @Test + void didNotInjectExampleComponent() { + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> this.applicationContext.getBean(ExampleComponent.class)); + } + + @Test + void flywayAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(FlywayAutoConfiguration.class)); + } + + @Test + void liquibaseAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(LiquibaseAutoConfiguration.class)); + } + + @Test + void cacheAutoConfigurationWasImported() { + assertThat(this.applicationContext).has(importedAutoConfiguration(CacheAutoConfiguration.class)); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestPropertiesIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestPropertiesIntegrationTests.java new file mode 100644 index 00000000000..7cc3e252628 --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestPropertiesIntegrationTests.java @@ -0,0 +1,43 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for the {@link JooqTest#properties properties} attribute of + * {@link JooqTest @JooqTest}. + * + * @author Artsiom Yudovin + */ +@JooqTest(properties = "spring.profiles.active=test") +class JooqTestPropertiesIntegrationTests { + + @Autowired + private Environment environment; + + @Test + void environmentWithNewProfile() { + assertThat(this.environment.getActiveProfiles()).containsExactly("test"); + } + +} diff --git a/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestWithAutoConfigureTestDatabaseIntegrationTests.java b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestWithAutoConfigureTestDatabaseIntegrationTests.java new file mode 100644 index 00000000000..1ee30574f7d --- /dev/null +++ b/spring-boot-project/spring-boot-test-autoconfigure/src/test/java/org/springframework/boot/test/autoconfigure/jooq/JooqTestWithAutoConfigureTestDatabaseIntegrationTests.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.test.autoconfigure.jooq; + +import javax.sql.DataSource; + +import org.jooq.DSLContext; +import org.jooq.SQLDialect; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.jdbc.EmbeddedDatabaseConnection; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link JooqTest @JooqTest}. + * + * @author Stephane Nicoll + */ +@JooqTest +@AutoConfigureTestDatabase(connection = EmbeddedDatabaseConnection.H2) +class JooqTestWithAutoConfigureTestDatabaseIntegrationTests { + + @Autowired + private DSLContext dsl; + + @Autowired + private DataSource dataSource; + + @Test + void replacesAutoConfiguredDataSource() throws Exception { + String product = this.dataSource.getConnection().getMetaData().getDatabaseProductName(); + assertThat(product).startsWith("H2"); + assertThat(this.dsl.configuration().dialect()).isEqualTo(SQLDialect.H2); + } + +} diff --git a/spring-boot-project/spring-boot/build.gradle b/spring-boot-project/spring-boot/build.gradle index 3b96ab0bf7d..b48c3902a99 100644 --- a/spring-boot-project/spring-boot/build.gradle +++ b/spring-boot-project/spring-boot/build.gradle @@ -73,6 +73,9 @@ dependencies { optional("org.hamcrest:hamcrest-library") optional("org.hibernate:hibernate-core-jakarta") optional("org.hibernate.validator:hibernate-validator") + optional("org.jooq:jooq") { + exclude(group: "javax.xml.bind", module: "jaxb-api") + } optional("org.liquibase:liquibase-core") { exclude(group: "javax.xml.bind", module: "jaxb-api") } diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/JooqDependsOnDatabaseInitializationDetector.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/JooqDependsOnDatabaseInitializationDetector.java new file mode 100644 index 00000000000..e45c8ab2505 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/JooqDependsOnDatabaseInitializationDetector.java @@ -0,0 +1,39 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.boot.jooq; + +import java.util.Collections; +import java.util.Set; + +import org.jooq.DSLContext; + +import org.springframework.boot.sql.init.dependency.AbstractBeansOfTypeDependsOnDatabaseInitializationDetector; +import org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector; + +/** + * {@link DependsOnDatabaseInitializationDetector} for jOOQ. + * + * @author Andy Wilkinson + */ +class JooqDependsOnDatabaseInitializationDetector extends AbstractBeansOfTypeDependsOnDatabaseInitializationDetector { + + @Override + protected Set> getDependsOnDatabaseInitializationBeanTypes() { + return Collections.singleton(DSLContext.class); + } + +} diff --git a/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/package-info.java b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/package-info.java new file mode 100644 index 00000000000..787bfaf53d4 --- /dev/null +++ b/spring-boot-project/spring-boot/src/main/java/org/springframework/boot/jooq/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright 2012-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Support for jOOQ. + * + * @see org.springframework.boot.json.JsonParser + */ +package org.springframework.boot.jooq; diff --git a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories index 2c8c5545536..995c173efbc 100644 --- a/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot/src/main/resources/META-INF/spring.factories @@ -93,4 +93,5 @@ org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\ org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector,\ org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\ +org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\ org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector