diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java index 46b962cf..2948f532 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfiguration.java @@ -35,7 +35,6 @@ import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.core.conversion.RelationalConverter; import org.springframework.data.relational.core.dialect.Dialect; -import org.springframework.data.relational.core.dialect.HsqlDbDialect; import org.springframework.data.relational.core.mapping.NamingStrategy; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; @@ -132,7 +131,7 @@ public class AbstractJdbcConfiguration { } @Bean - Dialect dialect() { - return HsqlDbDialect.INSTANCE; + public Dialect dialect(NamedParameterJdbcOperations template) { + return JdbcDialectResolver.getDialect(template); } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcDialectResolver.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcDialectResolver.java new file mode 100644 index 00000000..46da569d --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcDialectResolver.java @@ -0,0 +1,137 @@ +/* + * Copyright 2020 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.data.jdbc.repository.config; + +import java.awt.*; +import java.sql.Connection; +import java.sql.DatabaseMetaData; +import java.util.List; +import java.util.Optional; + +import javax.sql.DataSource; + +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.dao.NonTransientDataAccessException; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.HsqlDbDialect; +import org.springframework.data.relational.core.dialect.MySqlDialect; +import org.springframework.data.relational.core.dialect.PostgresDialect; +import org.springframework.data.relational.core.dialect.SqlServerDialect; +import org.springframework.data.util.Optionals; +import org.springframework.jdbc.core.ConnectionCallback; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; + +/** + * Resolves a {@link Dialect} from a {@link DataSource} using {@link JdbcDialectProvider}. Dialect resolution uses + * Spring's {@link SpringFactoriesLoader spring.factories} to determine available extensions. + * + * @author Jens Schauder + * @since 2.0 + * @see Dialect + * @see SpringFactoriesLoader + */ +public class JdbcDialectResolver { + + private static final List DETECTORS = SpringFactoriesLoader + .loadFactories(JdbcDialectProvider.class, JdbcDialectResolver.class.getClassLoader()); + + // utility constructor. + private JdbcDialectResolver() {} + + /** + * Retrieve a {@link Dialect} by inspecting a {@link DataSource}. + * + * @param template must not be {@literal null}. + * @return the resolved {@link Dialect} {@link NoDialectException} if the database type cannot be determined from + * {@link DataSource}. + * @throws NoDialectException if no {@link Dialect} can be found. + */ + public static Dialect getDialect(NamedParameterJdbcOperations template) { + + return DETECTORS.stream() // + .map(it -> it.getDialect(template)) // + .flatMap(Optionals::toStream) // + .findFirst() // + .orElseThrow(() -> new NoDialectException( + String.format("Cannot determine a dialect for %s. Please provide a Dialect.", + template))); + } + + /** + * SPI to extend Spring's default JDBC Dialect discovery mechanism. Implementations of this interface are discovered + * through Spring's {@link SpringFactoriesLoader} mechanism. + * + * @author Jens Schauder + * @see org.springframework.core.io.support.SpringFactoriesLoader + */ + public interface JdbcDialectProvider { + + /** + * Returns a {@link Dialect} for a {@link DataSource}. + * + * @param template the {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate} to be used with the {@link Dialect}. + * @return {@link Optional} containing the {@link Dialect} if the {@link JdbcDialectProvider} can provide a dialect + * object, otherwise {@link Optional#empty()}. + */ + Optional getDialect(NamedParameterJdbcOperations template); + } + + static public class DefaultDialectProvider implements JdbcDialectProvider { + + @Override + public Optional getDialect(NamedParameterJdbcOperations template) { + + return template.getJdbcOperations().execute((ConnectionCallback>) (connection) ->{ + + DatabaseMetaData metaData = connection.getMetaData(); + + String name = metaData.getDatabaseProductName().toLowerCase(); + + if (name.contains("hsql")) { + return Optional.of(HsqlDbDialect.INSTANCE); + } + if (name.contains("mysql")) { // catches also mariadb + return Optional.of(MySqlDialect.INSTANCE); + } + if (name.contains("postgresql")) { + return Optional.of(PostgresDialect.INSTANCE); + } + if (name.contains("microsoft")) { + return Optional.of(SqlServerDialect.INSTANCE); + } + + return Optional.empty(); + }); + + } + } + + /** + * Exception thrown when {@link JdbcDialectResolver} cannot resolve a {@link Dialect}. + */ + public static class NoDialectException extends NonTransientDataAccessException { + + /** + * Constructor for NoDialectFoundException. + * + * @param msg the detail message + */ + public NoDialectException(String msg) { + super(msg); + } + } + +} diff --git a/spring-data-jdbc/src/main/resources/META-INF/spring.factories b/spring-data-jdbc/src/main/resources/META-INF/spring.factories index f244d7b5..93520c82 100644 --- a/spring-data-jdbc/src/main/resources/META-INF/spring.factories +++ b/spring-data-jdbc/src/main/resources/META-INF/spring.factories @@ -1 +1,2 @@ org.springframework.data.repository.core.support.RepositoryFactorySupport=org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory +org.springframework.data.jdbc.repository.config.JdbcDialectResolver$JdbcDialectProvider=org.springframework.data.jdbc.repository.config.JdbcDialectResolver.DefaultDialectProvider \ No newline at end of file diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java index fde0dbfe..fd7ffc7c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/AbstractJdbcConfigurationIntegrationTests.java @@ -32,6 +32,8 @@ import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.HsqlDbDialect; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; @@ -58,7 +60,7 @@ public class AbstractJdbcConfigurationIntegrationTests { .map(context::getBean) // .forEach(it -> assertThat(it).isNotNull()); - }, AbstractJdbcConfiguration.class, Infrastructure.class); + }, AbstractJdbcConfigurationUnderTest.class, Infrastructure.class); } protected static void assertApplicationContext(Consumer verification, @@ -74,7 +76,7 @@ public class AbstractJdbcConfigurationIntegrationTests { } @Configuration - static class Infrastructure { + static class Infrastructure { @Bean public NamedParameterJdbcOperations jdbcOperations() { @@ -83,4 +85,14 @@ public class AbstractJdbcConfigurationIntegrationTests { return new NamedParameterJdbcTemplate(jdbcOperations); } } + + static class AbstractJdbcConfigurationUnderTest extends AbstractJdbcConfiguration { + + @Override + @Bean + public Dialect dialect(NamedParameterJdbcOperations template) { + return HsqlDbDialect.INSTANCE; + } + } + } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java index d4727ba8..573325e0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/EnableJdbcRepositoriesIntegrationTests.java @@ -156,5 +156,10 @@ public class EnableJdbcRepositoriesIntegrationTests { return new DefaultDataAccessStrategy(new SqlGeneratorSource(context, converter, dialect), context, converter, template); } + + @Bean + Dialect dialect(@Qualifier("qualifierJdbcOperations") NamedParameterJdbcOperations template) { + return JdbcDialectResolver.getDialect(template); + } } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfigurationIntegrationTests.java index 9969b6ea..5055e014 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfigurationIntegrationTests.java @@ -27,6 +27,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jdbc.core.convert.CascadingDataAccessStrategy; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; +import org.springframework.data.relational.core.dialect.Dialect; +import org.springframework.data.relational.core.dialect.HsqlDbDialect; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.test.util.ReflectionTestUtils; /** @@ -50,7 +53,7 @@ public class MyBatisJdbcConfigurationIntegrationTests extends AbstractJdbcConfig assertThat(strategies.get(0)).isInstanceOf(MyBatisDataAccessStrategy.class); }); - }, MyBatisJdbcConfiguration.class, MyBatisInfrastructure.class); + }, MyBatisJdbcConfigurationUnderTest.class, MyBatisInfrastructure.class); } @Configuration @@ -61,4 +64,13 @@ public class MyBatisJdbcConfigurationIntegrationTests extends AbstractJdbcConfig return mock(SqlSession.class); } } + + public static class MyBatisJdbcConfigurationUnderTest extends MyBatisJdbcConfiguration { + + @Override + @Bean + public Dialect dialect(NamedParameterJdbcOperations template) { + return HsqlDbDialect.INSTANCE; + } + } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java index 3be5ef41..f1f087ba 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/HsqlDataSourceConfiguration.java @@ -49,9 +49,4 @@ class HsqlDataSourceConfiguration { .addScript(TestUtils.createScriptName(context, "hsql")) // .build(); } - - @Bean - Dialect dialect() { - return HsqlDbDialect.INSTANCE; - } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MariaDBDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MariaDBDataSourceConfiguration.java index 385c0a56..fa6255e5 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MariaDBDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MariaDBDataSourceConfiguration.java @@ -22,11 +22,8 @@ import javax.script.ScriptException; import javax.sql.DataSource; import org.mariadb.jdbc.MariaDbDataSource; -import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; -import org.springframework.data.relational.core.dialect.Dialect; -import org.springframework.data.relational.core.dialect.MariaDbDialect; import org.testcontainers.containers.MariaDBContainer; import org.testcontainers.jdbc.ext.ScriptUtils; @@ -64,11 +61,6 @@ class MariaDBDataSourceConfiguration extends DataSourceConfiguration { } } - @Bean - Dialect dialect() { - return MariaDbDialect.INSTANCE; - } - @PostConstruct public void initDatabase() throws SQLException, ScriptException { ScriptUtils.executeSqlScript(createDataSource().getConnection(), null, "DROP DATABASE test;CREATE DATABASE test;"); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MsSqlDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MsSqlDataSourceConfiguration.java index 52d1f451..64aa23d2 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MsSqlDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MsSqlDataSourceConfiguration.java @@ -47,6 +47,7 @@ public class MsSqlDataSourceConfiguration extends DataSourceConfiguration { */ @Override protected DataSource createDataSource() { + SQLServerDataSource sqlServerDataSource = new SQLServerDataSource(); sqlServerDataSource.setURL(mssqlserver.getJdbcUrl()); sqlServerDataSource.setUser(mssqlserver.getUsername()); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java index f62872ba..334b0625 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/MySqlDataSourceConfiguration.java @@ -65,11 +65,6 @@ class MySqlDataSourceConfiguration extends DataSourceConfiguration { return dataSource; } - @Bean - Dialect dialect() { - return MySqlDialect.INSTANCE; - } - @PostConstruct public void initDatabase() throws SQLException, ScriptException { ScriptUtils.executeSqlScript(createDataSource().getConnection(), null, "DROP DATABASE test;CREATE DATABASE test;"); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java index 46f238af..1e96041f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/PostgresDataSourceConfiguration.java @@ -58,11 +58,6 @@ public class PostgresDataSourceConfiguration extends DataSourceConfiguration { return dataSource; } - @Bean - Dialect dialect() { - return PostgresDialect.INSTANCE; - } - /* * (non-Javadoc) * @see org.springframework.data.jdbc.testing.DataSourceFactoryBean#customizePopulator(org.springframework.jdbc.datasource.init.ResourceDatabasePopulator) diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java index d82dee46..07bf9f60 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/testing/TestConfiguration.java @@ -37,6 +37,7 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.SqlGeneratorSource; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.repository.config.JdbcDialectResolver; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; @@ -122,4 +123,9 @@ public class TestConfiguration { dialect.getIdentifierProcessing() ); } + + @Bean + Dialect dialect(NamedParameterJdbcOperations template) { + return JdbcDialectResolver.getDialect(template); + } } diff --git a/spring-data-jdbc/src/test/resources/container-license-acceptance.txt b/spring-data-jdbc/src/test/resources/container-license-acceptance.txt index b546fb08..ce512af3 100644 --- a/spring-data-jdbc/src/test/resources/container-license-acceptance.txt +++ b/spring-data-jdbc/src/test/resources/container-license-acceptance.txt @@ -1 +1 @@ -microsoft/mssql-server-linux:2017-CU6 \ No newline at end of file +mcr.microsoft.com/mssql/server:2017-CU12 \ No newline at end of file diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java deleted file mode 100644 index dd42bb2e..00000000 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MariaDbDialect.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2019 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.data.relational.core.dialect; - -/** - * The {@link Dialect} to be used with MariaDb. Since we haven't encountered any significant differences between MariaDb - * and Mysql its instance is actually {@link MySqlDialect#INSTANCE}, although that might change at any time. - * - * @author Jens Schauder - */ -public class MariaDbDialect extends MySqlDialect{ - - public static final Dialect INSTANCE = MySqlDialect.INSTANCE; - - protected MariaDbDialect() { } -} diff --git a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MariaDbDialectUnitTests.java b/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MariaDbDialectUnitTests.java deleted file mode 100644 index 4a2e9705..00000000 --- a/spring-data-relational/src/test/java/org/springframework/data/relational/core/dialect/MariaDbDialectUnitTests.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2019 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.data.relational.core.dialect; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.Test; - -/** - * Unit tests for {@link MariaDbDialect}. - * - * @author Jens Schauder - */ -public class MariaDbDialectUnitTests { - - @Test // DATAJDBC-278 - public void shouldNotSupportArrays() { - - ArrayColumns arrayColumns = MariaDbDialect.INSTANCE.getArraySupport(); - - assertThat(arrayColumns.isSupported()).isFalse(); - } - - @Test // DATAJDBC-278 - public void shouldRenderLimit() { - - LimitClause limit = MariaDbDialect.INSTANCE.limit(); - - assertThat(limit.getClausePosition()).isEqualTo(LimitClause.Position.AFTER_ORDER_BY); - assertThat(limit.getLimit(10)).isEqualTo("LIMIT 10"); - } - - @Test // DATAJDBC-278 - public void shouldRenderOffset() { - - LimitClause limit = MariaDbDialect.INSTANCE.limit(); - - assertThat(limit.getOffset(10)).isEqualTo("LIMIT 10, 18446744073709551615"); - } - - @Test // DATAJDBC-278 - public void shouldRenderLimitOffset() { - - LimitClause limit = MariaDbDialect.INSTANCE.limit(); - - assertThat(limit.getLimitOffset(20, 10)).isEqualTo("LIMIT 10, 20"); - } - - @Test // DATAJDBC-386 - public void shouldQuoteIdentifiersUsingBackticks() { - - String abcQuoted = MariaDbDialect.INSTANCE.getIdentifierProcessing().quote("abc"); - - assertThat(abcQuoted).isEqualTo("`abc`"); - } -}