From 6782ed97cbdd81a3d9962eb2790aad05f08097a7 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 4 Jun 2025 15:14:24 +0200 Subject: [PATCH 1/5] 1935-jdbc-sql-type-for-null - Prepare branch --- pom.xml | 2 +- spring-data-jdbc-distribution/pom.xml | 2 +- spring-data-jdbc/pom.xml | 4 ++-- spring-data-r2dbc/pom.xml | 4 ++-- spring-data-relational/pom.xml | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index ebd3103251..233bd4e77b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index b3c39e64c3..79ceae4c47 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index e61fd64020..671dfa56c9 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3ee76fd3c1..e85f4760f9 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8fd6d7a6f0..7eacd71180 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-1935-jdbc-sql-type-for-null-SNAPSHOT From 2024731051736248d555847e7fabe1b472451694 Mon Sep 17 00:00:00 2001 From: Sergey Korotaev Date: Tue, 3 Jun 2025 22:20:34 +0300 Subject: [PATCH 2/5] Add default JDBCType.NULL for null values. Signed-off-by: Sergey Korotaev Original pull request #2068 Closes #1935 --- .../core/convert/SqlParametersFactory.java | 10 +- .../convert/SqlParametersFactoryTest.java | 92 +++++++++++++++++++ 2 files changed, 97 insertions(+), 5 deletions(-) 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 8bf9bb869f..1905bc196d 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.core.convert; +import java.sql.JDBCType; import java.sql.SQLType; import java.util.ArrayList; import java.util.List; @@ -41,6 +42,7 @@ * @author Jens Schauder * @author Chirag Tailor * @author Mikhail Polivakha + * @author Sergey Korotaev * @since 2.4 */ public class SqlParametersFactory { @@ -187,11 +189,9 @@ private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSou private void addConvertedValue(SqlIdentifierParameterSource parameterSource, @Nullable Object value, SqlIdentifier paramName, Class javaType, SQLType sqlType) { - JdbcValue jdbcValue = converter.writeJdbcValue( // - value, // - javaType, // - sqlType // - ); + JdbcValue jdbcValue = value != null + ? converter.writeJdbcValue(value, javaType, sqlType) + : JdbcValue.of(null, JDBCType.NULL); parameterSource.addValue( // paramName, // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java index 9efdb3aeab..2d5e5ee6df 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java @@ -21,6 +21,7 @@ import static org.mockito.Mockito.*; import static org.springframework.data.jdbc.core.convert.DefaultDataAccessStrategyUnitTests.*; +import java.sql.JDBCType; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -32,6 +33,7 @@ import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; import org.springframework.data.jdbc.core.mapping.JdbcMappingContext; +import org.springframework.data.jdbc.core.mapping.JdbcValue; import org.springframework.data.relational.core.conversion.IdValueSource; import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.RelationalMappingContext; @@ -42,6 +44,7 @@ * Unit tests for {@link SqlParametersFactory}. * * @author Chirag Tailor + * @author Sergey Korotaev */ class SqlParametersFactoryTest { @@ -162,6 +165,69 @@ void parameterNamesGetSanitized() { assertThat(sqlParameterSource.getValue("val&ue")).isNull(); } + @Test + // GH-1935 + void enumParameterIsNotNullReturnCorrectSqlTypeFromConverter() { + + WithEnumEntity entity = new WithEnumEntity(23L, DummyEnum.ONE); + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + singletonList(WritingEnumConverter.INSTANCE)); + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, + WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + + assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); + assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); + assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(1111); + } + + @Test + // GH-1935 + void enumParameterIsNullReturnCorrectSqlTypeFromConverter() { + WithEnumEntity entity = new WithEnumEntity(23L, null); + + SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( + singletonList(WritingEnumConverter.INSTANCE)); + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, + WithEnumEntity.class, 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(); + } + + @Test + // GH-1935 + void enumParameterIsNotNullReturnCorrectSqlTypeWithoutConverter() { + + WithEnumEntity entity = new WithEnumEntity(23L, DummyEnum.ONE); + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, + WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + + assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); + assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); + assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(12); + + } + + @Test + // GH-1935 + void enumParameterIsNullReturnCorrectSqlTypeWithoutConverter() { + + WithEnumEntity entity = new WithEnumEntity(23L, null); + + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, + WithEnumEntity.class, 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(); + + } + @WritingConverter enum IdValueToStringConverter implements Converter { @@ -229,6 +295,17 @@ public String toString() { } } + @WritingConverter + enum WritingEnumConverter implements Converter { + + INSTANCE; + + @Override + public JdbcValue convert(DummyEnum source) { + return JdbcValue.of(source.name().toUpperCase(), JDBCType.OTHER); + } + } + @WritingConverter enum BooleanToStringConverter implements Converter { @@ -295,6 +372,21 @@ public WithIllegalCharacters(Long id, String value) { } } + private static class WithEnumEntity { + @Id Long id; + + DummyEnum dummyEnum; + + public WithEnumEntity(Long id, DummyEnum dummyEnum) { + this.id = id; + this.dummyEnum = dummyEnum; + } + } + + private enum DummyEnum { + ONE, TWO + } + private SqlParametersFactory createSqlParametersFactoryWithConverters(List converters) { MappingJdbcConverter converter = new MappingJdbcConverter(context, relationResolver, From 2ac4bbd0aba97b6aab26c5fc59cc94d93202ee89 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 4 Jun 2025 12:38:28 +0200 Subject: [PATCH 3/5] Polishing. Improved naming and formatting Original pull request #2068 See #1935 --- ...ava => SqlParametersFactoryUnitTests.java} | 47 ++++++++----------- 1 file changed, 19 insertions(+), 28 deletions(-) rename spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/{SqlParametersFactoryTest.java => SqlParametersFactoryUnitTests.java} (94%) diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java similarity index 94% rename from spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java rename to spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java index 2d5e5ee6df..f735f87a6c 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryTest.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/SqlParametersFactoryUnitTests.java @@ -46,7 +46,7 @@ * @author Chirag Tailor * @author Sergey Korotaev */ -class SqlParametersFactoryTest { +class SqlParametersFactoryUnitTests { RelationalMappingContext context = new JdbcMappingContext(); RelationResolver relationResolver = mock(RelationResolver.class); @@ -88,8 +88,7 @@ public void considersConfiguredWriteConverterForIdValueObjectsWhichReferencedInO assertThat(sqlParameterSource.getValue("DUMMYENTITYROOT")).isEqualTo(rawId); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void identifiersGetAddedAsParameters() { long id = 4711L; @@ -103,8 +102,7 @@ void identifiersGetAddedAsParameters() { assertThat(sqlParameterSource.getValue("reference")).isEqualTo(reference); } - @Test - // DATAJDBC-146 + @Test // DATAJDBC-146 void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { long id = 4711L; @@ -116,8 +114,7 @@ void additionalIdentifierForIdDoesNotLeadToDuplicateParameters() { assertThat(sqlParameterSource.getValue("id")).isEqualTo(id); } - @Test - // DATAJDBC-235 + @Test // DATAJDBC-235 void considersConfiguredWriteConverter() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -131,8 +128,7 @@ void considersConfiguredWriteConverter() { assertThat(sqlParameterSource.getValue("flag")).isEqualTo("T"); } - @Test - // DATAJDBC-412 + @Test // DATAJDBC-412 void considersConfiguredWriteConverterForIdValueObjects_onWrite() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( @@ -149,8 +145,7 @@ void considersConfiguredWriteConverterForIdValueObjects_onWrite() { assertThat(sqlParameterSource.getValue("value")).isEqualTo(value); } - @Test - // GH-1405 + @Test // GH-1405 void parameterNamesGetSanitized() { WithIllegalCharacters entity = new WithIllegalCharacters(23L, "aValue"); @@ -165,8 +160,7 @@ void parameterNamesGetSanitized() { assertThat(sqlParameterSource.getValue("val&ue")).isNull(); } - @Test - // GH-1935 + @Test // GH-1935 void enumParameterIsNotNullReturnCorrectSqlTypeFromConverter() { WithEnumEntity entity = new WithEnumEntity(23L, DummyEnum.ONE); @@ -174,38 +168,36 @@ void enumParameterIsNotNullReturnCorrectSqlTypeFromConverter() { SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( singletonList(WritingEnumConverter.INSTANCE)); - SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, - WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, WithEnumEntity.class, + Identifier.empty(), IdValueSource.PROVIDED); assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(1111); } - @Test - // GH-1935 + @Test // GH-1935 void enumParameterIsNullReturnCorrectSqlTypeFromConverter() { WithEnumEntity entity = new WithEnumEntity(23L, null); SqlParametersFactory sqlParametersFactory = createSqlParametersFactoryWithConverters( singletonList(WritingEnumConverter.INSTANCE)); - SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, - WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, WithEnumEntity.class, + 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(); } - @Test - // GH-1935 + @Test // GH-1935 void enumParameterIsNotNullReturnCorrectSqlTypeWithoutConverter() { WithEnumEntity entity = new WithEnumEntity(23L, DummyEnum.ONE); - SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, - WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, WithEnumEntity.class, + Identifier.empty(), IdValueSource.PROVIDED); assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); assertThat(sqlParameterSource.getValue("dummy_enum")).isEqualTo(DummyEnum.ONE.name()); @@ -213,14 +205,13 @@ void enumParameterIsNotNullReturnCorrectSqlTypeWithoutConverter() { } - @Test - // GH-1935 + @Test // GH-1935 void enumParameterIsNullReturnCorrectSqlTypeWithoutConverter() { WithEnumEntity entity = new WithEnumEntity(23L, null); - SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, - WithEnumEntity.class, Identifier.empty(), IdValueSource.PROVIDED); + SqlIdentifierParameterSource sqlParameterSource = sqlParametersFactory.forInsert(entity, WithEnumEntity.class, + Identifier.empty(), IdValueSource.PROVIDED); assertThat(sqlParameterSource.getValue("id")).isEqualTo(23L); assertThat(sqlParameterSource.getSqlType("dummy_enum")).isEqualTo(JDBCType.NULL.getVendorTypeNumber()); @@ -291,7 +282,7 @@ public int hashCode() { } public String toString() { - return "SqlParametersFactoryTest.IdValue(id=" + this.getId() + ")"; + return "SqlParametersFactoryUnitTests.IdValue(id=" + this.getId() + ")"; } } From bf1842c8cd4f4da1ec1c29a3f704e7054a059c77 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 4 Jun 2025 13:03:47 +0200 Subject: [PATCH 4/5] 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 7460931dab..7dec4f71e5 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.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 MappingJdbcConverter(RelationalMappingContext context, RelationResolver r 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 JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation colum 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 1905bc196d..799216cb05 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 @@ private void addConvertedPropertyValue(SqlIdentifierParameterSource parameterSou 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 2288a44c18..81a326c089 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 Timestamp convert(OffsetDateTime source) { 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 5728ce4f56..2e5eb6e6c5 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 @@ default JdbcArrayColumns getArraySupport() { 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 bc45ad3dda..cf59be0a69 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 Instant convert(DateTimeOffset source) { } } + + /** + * 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 0000000000..d4a700e656 --- /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 8b5f305149..f54a0fc392 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 IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(JdbcMappingCont */ @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 JdbcAggregateTemplate jdbcAggregateTemplate(ApplicationContext applicatio */ @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 DataAccessStrategy dataAccessStrategyBean(NamedParameterJdbcOperations op * 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 6198fab51e..26afdc6714 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.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 f735f87a6c..4184c52217 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.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 @@ void enumParameterIsNotNullReturnCorrectSqlTypeFromConverter() { 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 @@ void enumParameterIsNullReturnCorrectSqlTypeFromConverter() { 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 @@ void enumParameterIsNotNullReturnCorrectSqlTypeWithoutConverter() { 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 9c8ee97388..1e9b22ba38 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.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 @@ static class AbstractJdbcConfigurationUnderTest extends AbstractJdbcConfiguratio @Override @Bean - public Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + public JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return new DummyDialect(); } @@ -165,7 +166,7 @@ private static class Blah {} 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 b0ad7a4b1a..adab02fe43 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.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 static class MyBatisJdbcConfigurationUnderTest extends MyBatisJdbcConfigu @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 4ea56b1ee7..9a8f36eccd 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.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 @@ private List storeConverters(Dialect dialect) { @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 IdGeneratingEntityCallback idGeneratingBeforeSaveCallback(JdbcMappingCont } @Bean - Dialect jdbcDialect(NamedParameterJdbcOperations operations) { + JdbcDialect jdbcDialect(NamedParameterJdbcOperations operations) { return DialectResolver.getDialect(operations.getJdbcOperations()); } From 31901e5c71a285b76a6dbccc6e3c7f5b79b7d3ed Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Wed, 4 Jun 2025 15:50:04 +0200 Subject: [PATCH 5/5] Polishing. Remove unused fields in TestConfiguration. Original pull request #2068 See #1935 --- .../springframework/data/jdbc/testing/TestConfiguration.java | 4 ---- 1 file changed, 4 deletions(-) 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 9a8f36eccd..f09fbd0b71 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 @@ -22,9 +22,7 @@ import javax.sql.DataSource; -import org.apache.ibatis.session.SqlSessionFactory; import org.mockito.Mockito; -import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationEventPublisher; @@ -77,9 +75,7 @@ public class TestConfiguration { public static final String PROFILE_NO_SINGLE_QUERY_LOADING = "!" + PROFILE_SINGLE_QUERY_LOADING; @Autowired DataSource dataSource; - @Autowired BeanFactory beanFactory; @Autowired ApplicationEventPublisher publisher; - @Autowired(required = false) SqlSessionFactory sqlSessionFactory; @Bean JdbcRepositoryFactory jdbcRepositoryFactory(