DATAJDBC-455 - Polishing.
Rename Dialect bean to jdbcDialect to avoid generic bean names. Require JdbcOperations in DialectResolver. Rename JdbcDialectResolver to DialectResolver for consistent naming with R2DBC the underlying type is a Dialect. Tweak Javadoc and extend documentation with custom conversions include from Spring Data Commons. Extract MySQL identifier processing defaults into constant. Original pull request: #202.
This commit is contained in:
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<JdbcDialectProvider> 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<Dialect> getDialect(NamedParameterJdbcOperations template);
|
||||
Optional<Dialect> getDialect(JdbcOperations operations);
|
||||
}
|
||||
|
||||
static public class DefaultDialectProvider implements JdbcDialectProvider {
|
||||
|
||||
@Override
|
||||
public Optional<Dialect> getDialect(NamedParameterJdbcOperations template) {
|
||||
|
||||
JdbcOperations operations = template.getJdbcOperations();
|
||||
public Optional<Dialect> getDialect(JdbcOperations operations) {
|
||||
return Optional.ofNullable(operations.execute((ConnectionCallback<Dialect>) 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 {
|
||||
|
||||
@@ -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
|
||||
org.springframework.data.jdbc.repository.config.DialectResolver$JdbcDialectProvider=org.springframework.data.jdbc.repository.config.DialectResolver.DefaultDialectProvider
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class MyBatisJdbcConfigurationIntegrationTests extends AbstractJdbcConfig
|
||||
|
||||
@Override
|
||||
@Bean
|
||||
public Dialect dialect(NamedParameterJdbcOperations template) {
|
||||
public Dialect jdbcDialect(NamedParameterJdbcOperations operations) {
|
||||
return HsqlDbDialect.INSTANCE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
66
src/main/asciidoc/jdbc-custom-conversions.adoc
Normal file
66
src/main/asciidoc/jdbc-custom-conversions.adoc
Normal file
@@ -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<Boolean, String> {
|
||||
|
||||
@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<String, Boolean> {
|
||||
|
||||
@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]
|
||||
@@ -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<Boolean, String> {
|
||||
|
||||
INSTANCE;
|
||||
|
||||
@Override
|
||||
public String convert(Boolean source) {
|
||||
return source != null && source ? "T" : "F";
|
||||
}
|
||||
}
|
||||
|
||||
@ReadingConverter
|
||||
enum StringToBooleanConverter implements Converter<String, Boolean> {
|
||||
|
||||
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user