DATAJDBC-455 - Adds dynamic Dialect detection.

So far the user had to specify an `Dialect` themselves if they wanted to use anything but HSQLDB.
We now identify the supported databases and pick the appropriate `Dialect`.

Vendors who want to offer support for their database may provide an implementation of `JdbcDialectProvider` and register it using a  file under the key `org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider`.

Original pull request: #202.
This commit is contained in:
Jens Schauder
2020-03-23 17:03:53 +01:00
committed by Mark Paluch
parent 36192c4d97
commit 4c3fa05b6c
15 changed files with 180 additions and 128 deletions

View File

@@ -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);
}
}

View File

@@ -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<JdbcDialectProvider> 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<Dialect> getDialect(NamedParameterJdbcOperations template);
}
static public class DefaultDialectProvider implements JdbcDialectProvider {
@Override
public Optional<Dialect> getDialect(NamedParameterJdbcOperations template) {
return template.getJdbcOperations().execute((ConnectionCallback<Optional<Dialect>>) (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);
}
}
}

View File

@@ -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

View File

@@ -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<ConfigurableApplicationContext> 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;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -49,9 +49,4 @@ class HsqlDataSourceConfiguration {
.addScript(TestUtils.createScriptName(context, "hsql")) //
.build();
}
@Bean
Dialect dialect() {
return HsqlDbDialect.INSTANCE;
}
}

View File

@@ -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;");

View File

@@ -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());

View File

@@ -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;");

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -1 +1 @@
microsoft/mssql-server-linux:2017-CU6
mcr.microsoft.com/mssql/server:2017-CU12

View File

@@ -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() { }
}

View File

@@ -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`");
}
}