From bf1842c8cd4f4da1ec1c29a3f704e7054a059c77 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 4 Jun 2025 13:03:47 +0200 Subject: [PATCH] Dialect dependent handling of JDBC types for NULL. Not all databases support the JDBCSqlType.NULL. Therefore this handling was made dialect dependent, with SQL Server and DB2 using the old approach, while all others use JDBCSqlType.NULL In the process modified AbstractJdbcConfiguration to use JdbcDialect instead of Dialect. Original pull request #2068 See #1935 See #2031 --- .../core/convert/MappingJdbcConverter.java | 25 ++++++++------ .../core/convert/SqlParametersFactory.java | 4 +-- .../jdbc/core/dialect/JdbcDb2Dialect.java | 10 ++++++ .../data/jdbc/core/dialect/JdbcDialect.java | 11 +++++++ .../core/dialect/JdbcSqlServerDialect.java | 10 ++++++ .../jdbc/core/dialect/NullTypeStrategy.java | 33 +++++++++++++++++++ .../config/AbstractJdbcConfiguration.java | 12 +++---- .../config/MyBatisJdbcConfiguration.java | 4 +-- .../SqlParametersFactoryUnitTests.java | 7 ++-- ...ractJdbcConfigurationIntegrationTests.java | 5 +-- ...atisJdbcConfigurationIntegrationTests.java | 3 +- .../data/jdbc/testing/TestConfiguration.java | 11 +++---- 12 files changed, 100 insertions(+), 35 deletions(-) create mode 100644 spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/dialect/NullTypeStrategy.java 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()); }