From 3fec94ab4604d10e38f1698e484c24e0391b7798 Mon Sep 17 00:00:00 2001 From: Jens Schauder Date: Tue, 8 Jul 2025 13:53:35 +0200 Subject: [PATCH] Apply custom conversions for collections. The target type for the conversion of the complete collection gets adapted when a custom conversion was applied to the elements. Also `JdbcValue` elements as a result of a custom conversion get unwrapped. Closes #2078 Original pull request: #2081 --- .../core/convert/MappingJdbcConverter.java | 21 ++++++++++++++++ ...itoryCustomConversionIntegrationTests.java | 25 +++++++++++++++++++ ...oryCustomConversionIntegrationTests-h2.sql | 1 + ...yCustomConversionIntegrationTests-hsql.sql | 1 + ...tomConversionIntegrationTests-postgres.sql | 1 + .../MappingRelationalConverter.java | 25 +++++++++++++++++-- 6 files changed, 72 insertions(+), 2 deletions(-) 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..45245bae1 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 @@ -255,6 +255,9 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements } Class componentType = convertedValue.getClass().getComponentType(); + + if (convertedValue.getClass().isArray()) { + if (componentType != byte.class && componentType != Byte.class) { Object[] objectArray = requireObjectArray(convertedValue); @@ -268,6 +271,24 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements return JdbcValue.of(convertedValue, JDBCType.BINARY); } + return JdbcValue.of(convertedValue, sqlType); + } + + /** + * Unwraps values of type {@link JdbcValue}. + * + * @param convertedValue a value that might need unwrapping. + */ + @Override + @Nullable + protected Object unwrap(@Nullable Object convertedValue) { + + if (convertedValue instanceof JdbcValue jdbcValue) { + return jdbcValue.getValue(); + } + return convertedValue; + } + @SuppressWarnings("unchecked") @Override public R readAndResolve(TypeInformation type, RowDocument source, Identifier identifier) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java index d8f823f12..39eff0226 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java @@ -38,8 +38,10 @@ import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcValue; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; +import org.springframework.data.jdbc.testing.EnabledOnFeature; import org.springframework.data.jdbc.testing.IntegrationTest; import org.springframework.data.jdbc.testing.TestConfiguration; +import org.springframework.data.jdbc.testing.TestDatabaseFeatures; import org.springframework.data.repository.CrudRepository; /** @@ -61,6 +63,11 @@ public class JdbcRepositoryCustomConversionIntegrationTests { return factory.getRepository(EntityWithStringyBigDecimalRepository.class); } + @Bean + EntityWithDirectionsRepository repositoryWithDirections(JdbcRepositoryFactory factory) { + return factory.getRepository(EntityWithDirectionsRepository.class); + } + @Bean JdbcCustomConversions jdbcCustomConversions() { return new JdbcCustomConversions(asList(StringToBigDecimalConverter.INSTANCE, BigDecimalToString.INSTANCE, @@ -70,6 +77,7 @@ public class JdbcRepositoryCustomConversionIntegrationTests { } @Autowired EntityWithStringyBigDecimalRepository repository; + @Autowired EntityWithDirectionsRepository repositoryWithDirections; /** * In PostrgreSQL this fails if a simple converter like the following is used. @@ -162,6 +170,18 @@ public class JdbcRepositoryCustomConversionIntegrationTests { .containsExactly(Direction.CENTER); } + @Test // GH-2078 + @EnabledOnFeature(TestDatabaseFeatures.Feature.SUPPORTS_ARRAYS) + void saveAndLoadListOfDirectionsAsArray() { + + EntityWithDirections saved = repositoryWithDirections + .save(new EntityWithDirections(null, List.of(Direction.CENTER, Direction.RIGHT))); + + EntityWithDirections reloaded = repositoryWithDirections.findById(saved.id).orElseThrow(); + + assertThat(reloaded).isEqualTo(saved); + } + interface EntityWithStringyBigDecimalRepository extends CrudRepository { @Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION IN (:types)") @@ -171,6 +191,8 @@ public class JdbcRepositoryCustomConversionIntegrationTests { List findByEnumType(Direction type); } + interface EntityWithDirectionsRepository extends CrudRepository {} + private static class EntityWithStringyBigDecimal { @Id CustomId id; @@ -194,6 +216,9 @@ public class JdbcRepositoryCustomConversionIntegrationTests { Date created; } + record EntityWithDirections(@Id Long id, List directions) { + } + enum Direction { LEFT, CENTER, RIGHT } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql index 426153b9e..b6aebdd4d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql @@ -1,2 +1,3 @@ CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER); CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); +CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID IDENTITY PRIMARY KEY, DIRECTIONS INTEGER ARRAY); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql index 9508fbb0e..a56b15ecf 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql @@ -1,3 +1,4 @@ CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER); CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); +CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID IDENTITY PRIMARY KEY, DIRECTIONS INTEGER ARRAY); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql index 882d8df89..1692e6bcd 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql @@ -1,2 +1,3 @@ CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER); CREATE TABLE OTHER_ENTITY ( ID SERIAL PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); +CREATE TABLE ENTITY_WITH_DIRECTIONS ( ID SERIAL PRIMARY KEY, DIRECTIONS INTEGER ARRAY); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java index a80b6f1aa..d605dc730 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java @@ -777,14 +777,35 @@ public class MappingRelationalConverter extends AbstractRelationalConverter } for (Object o : value) { - mapped.add(writeValue(o, component)); + mapped.add(unwrap(writeValue(o, component))); } if (type.getType().isInstance(mapped) || !type.isCollectionLike()) { return mapped; } - return getConversionService().convert(mapped, type.getType()); + // if we succeeded converting the members of the collection, we actually ignore the fallback targetType since that + // was derived without considering custom conversions. + Class targetType = type.getType(); + if (!mapped.isEmpty()) { + + Class targetComponentType = mapped.get(0).getClass(); + targetType = Array.newInstance(targetComponentType, 0).getClass(); + } + return getConversionService().convert(mapped, targetType); + } + + /** + * Unwraps technology specific wrappers. Custom conversions may choose to return a wrapper class that contains additional information for the technology driver. + * These wrappers can't be used as members of a collection, therefore we may have to unwrap the values. + * + * This method allows technology specific implemenations to provide such an unwrapping mechanism. + * + * @param convertedValue a value that might need unwrapping. + */ + @Nullable + protected Object unwrap(@Nullable Object convertedValue) { + return convertedValue; } static Predicate isConstructorArgument(PersistentEntity entity) {