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 2948f532..b1347f0d 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 @@ -130,8 +130,17 @@ public class AbstractJdbcConfiguration { jdbcConverter, operations); } + /** + * Resolves a {@link Dialect JDBC dialect} by inspecting {@link NamedParameterJdbcOperations}. + * + * @param operations the {@link NamedParameterJdbcOperations} allowing access to a {@link java.sql.Connection}. + * @return + * @since 2.0 + * @throws org.springframework.data.jdbc.repository.config.DialectResolver.NoDialectException if the {@link Dialect} + * cannot be determined. + */ @Bean - public Dialect dialect(NamedParameterJdbcOperations template) { - return JdbcDialectResolver.getDialect(template); + public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + return DialectResolver.getDialect(operations.getJdbcOperations()); } } 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/DialectResolver.java similarity index 75% rename from spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/JdbcDialectResolver.java rename to spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/DialectResolver.java index f545a8c5..9013be20 100644 --- 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/DialectResolver.java @@ -19,6 +19,7 @@ import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.List; +import java.util.Locale; import java.util.Optional; import javax.sql.DataSource; @@ -34,42 +35,43 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.util.Optionals; import org.springframework.jdbc.core.ConnectionCallback; import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; /** - * Resolves a {@link Dialect} from a {@link DataSource} using {@link JdbcDialectProvider}. Dialect resolution uses - * Spring's {@link SpringFactoriesLoader spring.factories} to determine available extensions. + * Resolves a {@link Dialect}. Resolution typically uses {@link JdbcOperations} to obtain and inspect a + * {@link Connection}. Dialect resolution uses Spring's {@link SpringFactoriesLoader spring.factories} to determine + * available {@link JdbcDialectProvider extensions}. * * @author Jens Schauder * @since 2.0 * @see Dialect * @see SpringFactoriesLoader */ -public class JdbcDialectResolver { +public class DialectResolver { private static final List DETECTORS = SpringFactoriesLoader - .loadFactories(JdbcDialectProvider.class, JdbcDialectResolver.class.getClassLoader()); + .loadFactories(JdbcDialectProvider.class, DialectResolver.class.getClassLoader()); // utility constructor. - private JdbcDialectResolver() {} + private DialectResolver() {} /** - * Retrieve a {@link Dialect} by inspecting a {@link DataSource}. + * Retrieve a {@link Dialect} by inspecting a {@link Connection}. * - * @param template must not be {@literal null}. + * @param operations 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) { + public static Dialect getDialect(JdbcOperations operations) { return DETECTORS.stream() // - .map(it -> it.getDialect(template)) // + .map(it -> it.getDialect(operations)) // .flatMap(Optionals::toStream) // .findFirst() // .orElseThrow(() -> new NoDialectException( - String.format("Cannot determine a dialect for %s. Please provide a Dialect.", template))); + String.format("Cannot determine a dialect for %s. Please provide a Dialect.", operations))); } /** @@ -84,22 +86,18 @@ public class JdbcDialectResolver { /** * 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}. + * @param operations the {@link JdbcOperations} 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); + Optional getDialect(JdbcOperations operations); } static public class DefaultDialectProvider implements JdbcDialectProvider { @Override - public Optional getDialect(NamedParameterJdbcOperations template) { - - JdbcOperations operations = template.getJdbcOperations(); + public Optional getDialect(JdbcOperations operations) { return Optional.ofNullable(operations.execute((ConnectionCallback) DefaultDialectProvider::getDialect)); - } @Nullable @@ -107,7 +105,7 @@ public class JdbcDialectResolver { DatabaseMetaData metaData = connection.getMetaData(); - String name = metaData.getDatabaseProductName().toLowerCase(); + String name = metaData.getDatabaseProductName().toLowerCase(Locale.ENGLISH); if (name.contains("hsql")) { return HsqlDbDialect.INSTANCE; @@ -129,10 +127,12 @@ public class JdbcDialectResolver { // getIdentifierQuoteString() returns a space " " if identifier quoting is not // supported. - final String quoteString = metaData.getIdentifierQuoteString(); - final IdentifierProcessing.Quoting quoting = " ".equals(quoteString) ? IdentifierProcessing.Quoting.NONE : new IdentifierProcessing.Quoting(quoteString); + String quoteString = metaData.getIdentifierQuoteString(); + IdentifierProcessing.Quoting quoting = StringUtils.hasText(quoteString) + ? new IdentifierProcessing.Quoting(quoteString) + : IdentifierProcessing.Quoting.NONE; - final IdentifierProcessing.LetterCasing letterCasing; + IdentifierProcessing.LetterCasing letterCasing; // IdentifierProcessing tries to mimic the behavior of unquoted identifiers for their quoted variants. if (metaData.supportsMixedCaseIdentifiers()) { letterCasing = IdentifierProcessing.LetterCasing.AS_IS; @@ -144,12 +144,13 @@ public class JdbcDialectResolver { // But if it does happen, we go with the ANSI default. letterCasing = IdentifierProcessing.LetterCasing.UPPER_CASE; } + return IdentifierProcessing.create(quoting, letterCasing); } } /** - * Exception thrown when {@link JdbcDialectResolver} cannot resolve a {@link Dialect}. + * Exception thrown when {@link DialectResolver} cannot resolve a {@link Dialect}. */ public static class NoDialectException extends NonTransientDataAccessException { 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 93520c82..cc0d5cce 100644 --- a/spring-data-jdbc/src/main/resources/META-INF/spring.factories +++ b/spring-data-jdbc/src/main/resources/META-INF/spring.factories @@ -1,2 +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 +org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=org.springframework.data.jdbc.repository.config.DialectResolver.DefaultDialectProvider 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 fd7ffc7c..c0159db3 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 @@ -76,7 +76,7 @@ public class AbstractJdbcConfigurationIntegrationTests { } @Configuration - static class Infrastructure { + static class Infrastructure { @Bean public NamedParameterJdbcOperations jdbcOperations() { @@ -90,7 +90,7 @@ public class AbstractJdbcConfigurationIntegrationTests { @Override @Bean - public Dialect dialect(NamedParameterJdbcOperations template) { + public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { 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 573325e0..87d2ec74 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 @@ -158,8 +158,8 @@ public class EnableJdbcRepositoriesIntegrationTests { } @Bean - Dialect dialect(@Qualifier("qualifierJdbcOperations") NamedParameterJdbcOperations template) { - return JdbcDialectResolver.getDialect(template); + Dialect jdbcDialect(@Qualifier("qualifierJdbcOperations") NamedParameterJdbcOperations operations) { + return DialectResolver.getDialect(operations.getJdbcOperations()); } } } 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 5055e014..bf373417 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 @@ -69,7 +69,7 @@ public class MyBatisJdbcConfigurationIntegrationTests extends AbstractJdbcConfig @Override @Bean - public Dialect dialect(NamedParameterJdbcOperations template) { + public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { return HsqlDbDialect.INSTANCE; } } 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 07bf9f60..75ac9274 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,7 +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.config.DialectResolver; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.data.relational.core.mapping.NamingStrategy; @@ -125,7 +125,7 @@ public class TestConfiguration { } @Bean - Dialect dialect(NamedParameterJdbcOperations template) { - return JdbcDialectResolver.getDialect(template); + Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + return DialectResolver.getDialect(operations.getJdbcOperations()); } } diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java index cc78ebcf..102f3416 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/dialect/MySqlDialect.java @@ -18,6 +18,7 @@ package org.springframework.data.relational.core.dialect; import org.springframework.data.relational.core.sql.IdentifierProcessing; import org.springframework.data.relational.core.sql.IdentifierProcessing.LetterCasing; import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting; +import org.springframework.util.Assert; /** * A SQL dialect for MySQL. @@ -28,17 +29,33 @@ import org.springframework.data.relational.core.sql.IdentifierProcessing.Quoting */ public class MySqlDialect extends AbstractDialect { + /** + * MySQL defaults for {@link IdentifierProcessing}. + */ + public static final IdentifierProcessing MYSQL_IDENTIFIER_PROCESSING = IdentifierProcessing.create(new Quoting("`"), + LetterCasing.LOWER_CASE); + /** * Singleton instance. */ public static final MySqlDialect INSTANCE = new MySqlDialect(); - private IdentifierProcessing identifierProcessing; + + private final IdentifierProcessing identifierProcessing; protected MySqlDialect() { - this(IdentifierProcessing.create(new Quoting("`"), LetterCasing.LOWER_CASE)); + this(MYSQL_IDENTIFIER_PROCESSING); } + /** + * Creates a new {@link MySqlDialect} given {@link IdentifierProcessing}. + * + * @param identifierProcessing must not be null. + * @since 2.0 + */ public MySqlDialect(IdentifierProcessing identifierProcessing) { + + Assert.notNull(identifierProcessing, "IdentifierProcessing must not be null"); + this.identifierProcessing = identifierProcessing; } @@ -94,6 +111,10 @@ public class MySqlDialect extends AbstractDialect { return LIMIT_CLAUSE; } + /* + * (non-Javadoc) + * @see org.springframework.data.relational.core.dialect.Dialect#getIdentifierProcessing() + */ @Override public IdentifierProcessing getIdentifierProcessing() { return identifierProcessing; diff --git a/src/main/asciidoc/jdbc-custom-conversions.adoc b/src/main/asciidoc/jdbc-custom-conversions.adoc new file mode 100644 index 00000000..ea4c5fd2 --- /dev/null +++ b/src/main/asciidoc/jdbc-custom-conversions.adoc @@ -0,0 +1,66 @@ +[[jdbc.custom-converters]] +== Custom Conversions + +Spring Data JDBC allows registration of custom converters to influence how values are mapped in the database. +Currently, converters are only applied on property-level. + +[[jdbc.custom-converters.writer]] +=== Writing a Property by Using a Registered Spring Converter + +The following example shows an implementation of a `Converter` that converts from a `Boolean` object to a `String` value: + +[source,java] +---- +import org.springframework.core.convert.converter.Converter; + +@WritingConverter +public class BooleanToStringConverter implements Converter { + + @Override + public String convert(Boolean source) { + return source != null && source ? "T" : "F"; + } +} +---- + +There are a couple of things to notice here: `Boolean` and `String` are both simple types hence Spring Data requires a hint in which direction this converter should apply (reading or writing). +By annotating this converter with `@WritingConverter` you instruct Spring Data to write every `Boolean` property as `String` in the database. + +[[jdbc.custom-converters.reader]] +=== Reading by Using a Spring Converter + +The following example shows an implementation of a `Converter` that converts from a `String` to a `Boolean` value: + +[source,java] +---- +@ReadingConverter +public class StringToBooleanConverter implements Converter { + + @Override + public Boolean convert(String source) { + return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE; + } +} +---- + +There are a couple of things to notice here: `String` and `Boolean` are both simple types hence Spring Data requires a hint in which direction this converter should apply (reading or writing). +By annotating this converter with `@ReadingConverter` you instruct Spring Data to convert every `String` value from the database that should be assigned to a `Boolean` property. + +[[jdbc.custom-converters.configuration]] +=== Registering Spring Converters with the `JdbcConverter` + +[source,java] +---- +class MyJdbcConfiguration extends AbstractJdbcConfiguration { + + // … + + @Overwrite + @Bean + public JdbcCustomConversions jdbcCustomConversions() { + return new JdbcCustomConversions(Arrays.asList(new BooleanToStringConverter(), new StringToBooleanConverter())); + } +} +---- + +include::{spring-data-commons-docs}/custom-conversions.adoc[leveloffset=+3] diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index f9cf8b41..b0e070a9 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -67,29 +67,28 @@ The Spring Data JDBC repositories support can be activated by an annotation thro .Spring Data JDBC repositories using Java configuration ==== -[source, java] +[source,java] ---- @Configuration -@EnableJdbcRepositories // <1> -class ApplicationConfig extends AbstractJdbcConfiguration { // <2> +@EnableJdbcRepositories // <1> +class ApplicationConfig extends AbstractJdbcConfiguration { // <2> - @Bean - public DataSource dataSource() { // <3> + @Bean + public DataSource dataSource() { // <3> - EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); - return builder.setType(EmbeddedDatabaseType.HSQL).build(); - } + EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder(); + return builder.setType(EmbeddedDatabaseType.HSQL).build(); + } - @Bean - NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) { // <4> + @Bean + NamedParameterJdbcOperations namedParameterJdbcOperations(DataSource dataSource) { // <4> + return new NamedParameterJdbcTemplate(dataSource); + } - return new NamedParameterJdbcTemplate(dataSource); - } - - @Bean - TransactionManager transactionManager(DataSource dataSource) { // <5> - return new DataSourceTransactionManager(dataSource); - } + @Bean + TransactionManager transactionManager(DataSource dataSource) { // <5> + return new DataSourceTransactionManager(dataSource); + } } ---- <1> `@EnableJdbcRepositories` creates implementations for interfaces derived from `Repository` @@ -101,7 +100,7 @@ class ApplicationConfig extends AbstractJdbcConfiguration { // <2> The configuration class in the preceding example sets up an embedded HSQL database by using the `EmbeddedDatabaseBuilder` API of `spring-jdbc`. The `DataSource` is then used to set up `NamedParameterJdbcOperations` and a `TransactionManager. -We finally activate Spring Data JDBC repositories by using the `@EnableJdbcRepositories`. + We finally activate Spring Data JDBC repositories by using the `@EnableJdbcRepositories`. If no base package is configured, it uses the package in which the configuration class resides. Extending `AbstractJdbcConfiguration` ensures various beans get registered. Overwriting its methods can be used to customize the setup (see below). @@ -110,49 +109,15 @@ This configuration can be further simplified by using Spring Boot. With Spring Boot a `DataSource` is sufficient once the starter `spring-boot-starter-data-jdbc` is included in the dependencies. Everything else is done by Spring Boot. -There are a couple of things one might want to customize in this setup - -You can register custom conversions which will be used when writing to or reading from the database by overwriting `jdbcCustomConversions` as demonstrated in the following example: -==== -[source, java] ----- - @Overwrite - @Bean - public JdbcCustomConversions jdbcCustomConversions() { - return new JdbcCustomConversions(new JdbcCustomConversions(Arrays.asList(BooleanToStringConverter.INSTANCE, StringToBooleanConverter.INSTANCE))); - } - - - @WritingConverter - enum BooleanToStringConverter implements Converter { - - INSTANCE; - - @Override - public String convert(Boolean source) { - return source != null && source ? "T" : "F"; - } - } - - @ReadingConverter - enum StringToBooleanConverter implements Converter { - - INSTANCE; - - @Override - public Boolean convert(String source) { - return source != null && source.equalsIgnoreCase("T") ? Boolean.TRUE : Boolean.FALSE; - } - } ----- -==== +There are a couple of things one might want to customize in this setup. Spring Data JDBC uses implementations of the interface `Dialect` to encapsulate behavior that is specific to a database or its JDBC driver. By default the `AbstractJdbcConfiguration` tries to determine the database in use an register the correct `Dialect`. -This behavior can be changed by overwriting `dialect(NamedParameterJdbcOperations)`. +This behavior can be changed by overwriting `jdbcDialect(NamedParameterJdbcOperations)`. -Database vendors may provide an implementation of `JdbcDialectProvider` along with a matching entry in the `META-INF/spring-factories` file registering it for the key `org.springframework.data.jdbc.repository.config.JdbcDialectResolver$JdbcDialectProvider`. -The `JdbcDialectProvider` should detect the database and provide an appropriate `Dialect` implementation for it. +TIP: Dialects are resolved by [`JdbcDialectResolver`] from `JdbcOperations`, typically by inspecting `Connection`. ++ You can let Spring auto-discover your `Dialect` by registering a class that implements `org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider` through `META-INF/spring.factories`. +`DialectResolver` discovers dialect provider implementations from the class path using Spring's `SpringFactoriesLoader`. [[jdbc.entity-persistence]] == Persisting Entities @@ -759,6 +724,8 @@ Spring Data JDBC uses the `EntityCallback` API for its auditing support and reac | After an aggregate root gets created from a database `ResultSet` and all its property get set. |=== +include::jdbc-custom-conversions.adoc[] + [[jdbc.logging]] == Logging