diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java index 7460931da..7dec4f71e 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java @@ -31,6 +31,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.jdbc.core.dialect.NullTypeStrategy; import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.core.mapping.JdbcValue; import org.springframework.data.jdbc.support.JdbcUtil; @@ -73,6 +74,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements private final JdbcTypeFactory typeFactory; private final RelationResolver relationResolver; + private final NullTypeStrategy nullTypeStrategy; /** * Creates a new {@link MappingJdbcConverter} given {@link MappingContext} and a {@link JdbcTypeFactory#unsupported() @@ -84,15 +86,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements * @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}. */ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) { - - super(context, new JdbcCustomConversions()); - - Assert.notNull(relationResolver, "RelationResolver must not be null"); - - this.typeFactory = JdbcTypeFactory.unsupported(); - this.relationResolver = relationResolver; - - registerAggregateReferenceConverters(); + this(context, relationResolver, new JdbcCustomConversions(), JdbcTypeFactory.unsupported(), NullTypeStrategy.DEFAULT); } /** @@ -105,13 +99,20 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory) { + this(context, relationResolver, conversions, typeFactory, NullTypeStrategy.DEFAULT); + } + + public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver, CustomConversions conversions, JdbcTypeFactory typeFactory, NullTypeStrategy nullTypeStrategy) { + super(context, conversions); Assert.notNull(typeFactory, "JdbcTypeFactory must not be null"); Assert.notNull(relationResolver, "RelationResolver must not be null"); + Assert.notNull(nullTypeStrategy, "NullTypeStrategy must not be null"); this.typeFactory = typeFactory; this.relationResolver = relationResolver; + this.nullTypeStrategy = nullTypeStrategy; registerAggregateReferenceConverters(); } @@ -250,7 +251,11 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements return result; } - if (convertedValue == null || !convertedValue.getClass().isArray()) { + if (convertedValue == null ) { + return JdbcValue.of(null, nullTypeStrategy.getNullType(sqlType)); + } + + if (!convertedValue.getClass().isArray()) { return JdbcValue.of(convertedValue, sqlType); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java index 1905bc196..799216cb0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/SqlParametersFactory.java @@ -189,9 +189,7 @@ public class SqlParametersFactory { private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, SqlIdentifier paramName, Class javaType, SQLType sqlType) { - JdbcValue jdbcValue = value != null - ? converter.writeJdbcValue(value, javaType, sqlType) - : JdbcValue.of(null, JDBCType.NULL); + JdbcValue jdbcValue = converter.writeJdbcValue(value, javaType, sqlType); parameterSource.addValue( // paramName, // diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java index 2288a44c1..81a326c08 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDb2Dialect.java @@ -66,4 +66,14 @@ public class JdbcDb2Dialect extends Db2Dialect implements JdbcDialect { return Timestamp.from(source.toInstant()); } } + + /** + * DB2 does not support {@link java.sql.JDBCType#NULL}. Therefore it uses {@link NullTypeStrategy#NOOP}. + * + * @since 4.0 + */ + @Override + public NullTypeStrategy getNullTypeStrategy() { + return NullTypeStrategy.NOOP; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java index 5728ce4f5..2e5eb6e6c 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcDialect.java @@ -37,4 +37,15 @@ public interface JdbcDialect extends Dialect { return JdbcArrayColumns.Unsupported.INSTANCE; } + /** + * Determines how to handle the {@link java.sql.JDBCType} of {@literal null} values. + * + * The default is suitable for all databases supporting {@link java.sql.JDBCType#NULL}. + * + * @return a strategy to handle the {@link java.sql.JDBCType} of {@literal null} values. Guaranteed not to be null. + * @since 4.0 + */ + default NullTypeStrategy getNullTypeStrategy() { + return NullTypeStrategy.DEFAULT; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java index bc45ad3dd..cf59be0a6 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/JdbcSqlServerDialect.java @@ -70,4 +70,14 @@ public class JdbcSqlServerDialect extends SqlServerDialect implements JdbcDialec } } + + /** + * SQL Server does not support {@link java.sql.JDBCType#NULL}. Therefore it uses {@link NullTypeStrategy#NOOP}. + * + * @since 4.0 + */ + @Override + public NullTypeStrategy getNullTypeStrategy() { + return NullTypeStrategy.NOOP; + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/NullTypeStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/NullTypeStrategy.java new file mode 100644 index 000000000..d4a700e65 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/NullTypeStrategy.java @@ -0,0 +1,33 @@ +package org.springframework.data.jdbc.core.dialect; + +import java.sql.JDBCType; +import java.sql.SQLType; + +/** + * Interface for defining what to {@link SQLType} to use for {@literal null} values. + * + * @author Jens Schauder + * @since 4.0 + */ +public interface NullTypeStrategy { + + /** + * Implementation that always uses {@link JDBCType#NULL}. Suitable for all databases that actually support this + * {@link JDBCType}. + */ + NullTypeStrategy DEFAULT = sqlType -> JDBCType.NULL; + + /** + * Implementation that uses what ever type was past in as an argument. Suitable for databases that do not support + * {@link JDBCType#NULL}. + */ + NullTypeStrategy NOOP = sqlType -> sqlType; + + /** + * {@link SQLType} to use for {@literal null} values. + * + * @param sqlType a fallback value that is considered suitable by the caller. + * @return Guaranteed not to be {@literal null}. + */ + SQLType getNullType(SQLType sqlType); +} 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 8b5f30514..f54a0fc39 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 @@ -147,14 +147,12 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { */ @Bean public JdbcConverter jdbcConverter(JdbcMappingContext mappingContext, NamedParameterJdbcOperations operations, - @Lazy RelationResolver relationResolver, JdbcCustomConversions conversions, Dialect dialect) { + @Lazy RelationResolver relationResolver, JdbcCustomConversions conversions, JdbcDialect dialect) { - org.springframework.data.jdbc.core.dialect.JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect jd - ? jd.getArraySupport() - : JdbcArrayColumns.DefaultSupport.INSTANCE; + org.springframework.data.jdbc.core.dialect.JdbcArrayColumns arrayColumns = dialect.getArraySupport(); DefaultJdbcTypeFactory jdbcTypeFactory = new DefaultJdbcTypeFactory(operations.getJdbcOperations(), arrayColumns); - return new MappingJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory); + return new MappingJdbcConverter(mappingContext, relationResolver, conversions, jdbcTypeFactory, dialect.getNullTypeStrategy()); } /** @@ -222,7 +220,7 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { */ @Bean public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations operations, JdbcConverter jdbcConverter, - JdbcMappingContext context, Dialect dialect) { + JdbcMappingContext context, JdbcDialect dialect) { SqlGeneratorSource sqlGeneratorSource = new SqlGeneratorSource(context, jdbcConverter, dialect); DataAccessStrategyFactory factory = new DataAccessStrategyFactory(sqlGeneratorSource, jdbcConverter, operations, @@ -242,7 +240,7 @@ public class AbstractJdbcConfiguration implements ApplicationContextAware { * cannot be determined. */ @Bean - public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + public JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return DialectResolver.getDialect(operations.getJdbcOperations()); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfiguration.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfiguration.java index 6198fab51..26afdc671 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfiguration.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/config/MyBatisJdbcConfiguration.java @@ -24,9 +24,9 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.jdbc.core.convert.DataAccessStrategy; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.QueryMappingConfiguration; +import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; -import org.springframework.data.relational.core.dialect.Dialect; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; /** @@ -46,7 +46,7 @@ public class MyBatisJdbcConfiguration extends AbstractJdbcConfiguration { @Bean @Override public DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations operations, JdbcConverter jdbcConverter, - JdbcMappingContext context, Dialect dialect) { + JdbcMappingContext context, JdbcDialect dialect) { return MyBatisDataAccessStrategy.createCombinedAccessStrategy(context, jdbcConverter, operations, session, dialect, queryMappingConfiguration.orElse(QueryMappingConfiguration.EMPTY)); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java index f735f87a6..4184c5221 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java @@ -22,6 +22,7 @@ import static org.mockito.Mockito.*; import static org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategyUnitTests.*; import java.sql.JDBCType; +import java.sql.Types; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -173,7 +174,7 @@ class SqlParametersFactoryUnitTests { assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); - assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(1111); + assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(Types.OTHER); } @Test // GH-1935 @@ -187,8 +188,8 @@ class SqlParametersFactoryUnitTests { Identifier.empty(), IdValueSource.PROVIDED); assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); - assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(JDBCType.NULL.getVendorTypeNumber()); assertThat(sqlParameterSource.getValue("dummy_enum")).isNull(); + assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(Types.NULL); } @Test // GH-1935 @@ -201,7 +202,7 @@ class SqlParametersFactoryUnitTests { assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); - assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(12); + assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(Types.VARCHAR); } 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 9c8ee9738..1e9b22ba3 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 @@ -36,6 +36,7 @@ import org.springframework.data.jdbc.core.JdbcAggregateTemplate; 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.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.relational.RelationalManagedTypes; import org.springframework.data.relational.core.dialect.Dialect; @@ -142,7 +143,7 @@ class AbstractJdbcConfigurationIntegrationTests { @Override @Bean - public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + public JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return new DummyDialect(); } @@ -165,7 +166,7 @@ class AbstractJdbcConfigurationIntegrationTests { private static class Blubb {} - private static class DummyDialect implements Dialect { + private static class DummyDialect implements JdbcDialect { @Override public LimitClause limit() { return null; 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 b0ad7a4b1..adab02fe4 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 @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Bean; 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.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.dialect.JdbcHsqlDbDialect; import org.springframework.data.jdbc.mybatis.MyBatisDataAccessStrategy; import org.springframework.data.relational.core.dialect.Dialect; @@ -70,7 +71,7 @@ public class MyBatisJdbcConfigurationIntegrationTests extends AbstractJdbcConfig @Override @Bean - public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + public JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return JdbcHsqlDbDialect.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 4ea56b1ee..9a8f36ecc 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 @@ -36,7 +36,6 @@ import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Profile; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.convert.*; -import org.springframework.data.jdbc.core.dialect.JdbcArrayColumns; import org.springframework.data.jdbc.core.dialect.JdbcDialect; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; @@ -162,17 +161,15 @@ public class TestConfiguration { @Bean JdbcConverter relationalConverter(RelationalMappingContext mappingContext, @Lazy RelationResolver relationResolver, CustomConversions conversions, @Qualifier("namedParameterJdbcTemplate") NamedParameterJdbcOperations template, - Dialect dialect) { + JdbcDialect dialect) { - org.springframework.data.jdbc.core.dialect.JdbcArrayColumns arrayColumns = dialect instanceof JdbcDialect - ? ((JdbcDialect) dialect).getArraySupport() - : JdbcArrayColumns.DefaultSupport.INSTANCE; + org.springframework.data.jdbc.core.dialect.JdbcArrayColumns arrayColumns = dialect.getArraySupport(); return new MappingJdbcConverter( // mappingContext, // relationResolver, // conversions, // - new DefaultJdbcTypeFactory(template.getJdbcOperations(), arrayColumns)); + new DefaultJdbcTypeFactory(template.getJdbcOperations(), arrayColumns), dialect.getNullTypeStrategy()); } /** @@ -188,7 +185,7 @@ public class TestConfiguration { } @Bean - Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return DialectResolver.getDialect(operations.getJdbcOperations()); }