diff --git a/pom.xml b/pom.xml index eaee823fa0..deb60a8dbb 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-relational-parent - 4.0.0-SNAPSHOT + 4.0.0-2078-custom-converter-in-collection-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index b3c39e64c3..b422d3dde8 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-2078-custom-converter-in-collection-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index e61fd64020..e8a3ff4ed5 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-2078-custom-converter-in-collection-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-2078-custom-converter-in-collection-SNAPSHOT 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 2c3feffdb6..6de2102eed 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,9 +255,11 @@ public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation colum } Class componentType = convertedValue.getClass().getComponentType(); + if (componentType != byte.class && componentType != Byte.class) { Object[] objectArray = requireObjectArray(convertedValue); + return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY); } @@ -268,6 +270,21 @@ public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation colum return JdbcValue.of(convertedValue, JDBCType.BINARY); } + /** + * 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 d8f823f12d..39eff02265 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.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 @@ EntityWithStringyBigDecimalRepository repository(JdbcRepositoryFactory factory) 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 @@ JdbcCustomConversions jdbcCustomConversions() { } @Autowired EntityWithStringyBigDecimalRepository repository; + @Autowired EntityWithDirectionsRepository repositoryWithDirections; /** * In PostrgreSQL this fails if a simple converter like the following is used. @@ -162,6 +170,18 @@ void queryByEnumTypeEqual() { .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 @@ interface EntityWithStringyBigDecimalRepository extends CrudRepository findByEnumType(Direction type); } + interface EntityWithDirectionsRepository extends CrudRepository {} + private static class EntityWithStringyBigDecimal { @Id CustomId id; @@ -194,6 +216,9 @@ private static class OtherEntity { 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 426153b9e3..b6aebdd4df 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 9508fbb0e2..a56b15ecf2 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 882d8df894..1692e6bcdc 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-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3ee76fd3c1..9210bb285c 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-2078-custom-converter-in-collection-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-2078-custom-converter-in-collection-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 8fd6d7a6f0..31238eea28 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-2078-custom-converter-in-collection-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-2078-custom-converter-in-collection-SNAPSHOT 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 82e7e133c3..9c123c0244 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 @@ -44,16 +44,7 @@ import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PersistentPropertyPathAccessor; import org.springframework.data.mapping.context.MappingContext; -import org.springframework.data.mapping.model.CachingValueExpressionEvaluatorFactory; -import org.springframework.data.mapping.model.ConvertingPropertyAccessor; -import org.springframework.data.mapping.model.EntityInstantiator; -import org.springframework.data.mapping.model.ParameterValueProvider; -import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; -import org.springframework.data.mapping.model.PropertyValueProvider; -import org.springframework.data.mapping.model.SimpleTypeHolder; -import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mapping.model.ValueExpressionEvaluator; -import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider; +import org.springframework.data.mapping.model.*; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.projection.EntityProjectionIntrospector.ProjectionPredicate; @@ -790,14 +781,35 @@ private Object writeCollection(Iterable value, TypeInformation type) { } 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) {