diff --git a/pom.xml b/pom.xml
index ebd3103251..1458884755 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
org.springframework.dataspring-data-relational-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-1828-aggregate-ref-with-convertable-SNAPSHOTpomSpring Data Relational Parent
diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml
index b3c39e64c3..b6d1148cbe 100644
--- a/spring-data-jdbc-distribution/pom.xml
+++ b/spring-data-jdbc-distribution/pom.xml
@@ -14,7 +14,7 @@
org.springframework.dataspring-data-relational-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-1828-aggregate-ref-with-convertable-SNAPSHOT../pom.xml
diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml
index e61fd64020..0eecd3490a 100644
--- a/spring-data-jdbc/pom.xml
+++ b/spring-data-jdbc/pom.xml
@@ -6,7 +6,7 @@
4.0.0spring-data-jdbc
- 4.0.0-SNAPSHOT
+ 4.0.0-1828-aggregate-ref-with-convertable-SNAPSHOTSpring Data JDBCSpring Data module for JDBC repositories.
@@ -15,7 +15,7 @@
org.springframework.dataspring-data-relational-parent
- 4.0.0-SNAPSHOT
+ 4.0.0-1828-aggregate-ref-with-convertable-SNAPSHOT
diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java
deleted file mode 100644
index b3d0a2ce3c..0000000000
--- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java
+++ /dev/null
@@ -1,136 +0,0 @@
-/*
- * Copyright 2021-2025 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.jdbc.core.convert;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-import org.springframework.core.ResolvableType;
-import org.springframework.core.convert.ConversionService;
-import org.springframework.core.convert.TypeDescriptor;
-import org.springframework.core.convert.converter.GenericConverter;
-import org.springframework.data.convert.ReadingConverter;
-import org.springframework.data.convert.WritingConverter;
-import org.springframework.data.jdbc.core.mapping.AggregateReference;
-import org.springframework.lang.Nullable;
-
-/**
- * Converters for aggregate references. They need a {@link ConversionService} in order to delegate the conversion of the
- * content of the {@link AggregateReference}.
- *
- * @author Jens Schauder
- * @author Mark Paluch
- * @since 2.3
- */
-class AggregateReferenceConverters {
-
- /**
- * Returns the converters to be registered.
- *
- * @return a collection of converters. Guaranteed to be not {@literal null}.
- */
- public static Collection getConvertersToRegister(ConversionService conversionService) {
-
- return Arrays.asList(new AggregateReferenceToSimpleTypeConverter(conversionService),
- new SimpleTypeToAggregateReferenceConverter(conversionService));
- }
-
- /**
- * Converts from an AggregateReference to its id, leaving the conversion of the id to the ultimate target type to the
- * delegate {@link ConversionService}.
- */
- @WritingConverter
- private static class AggregateReferenceToSimpleTypeConverter implements GenericConverter {
-
- private static final Set CONVERTIBLE_TYPES = Collections
- .singleton(new ConvertiblePair(AggregateReference.class, Object.class));
-
- private final ConversionService delegate;
-
- AggregateReferenceToSimpleTypeConverter(ConversionService delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public Set getConvertibleTypes() {
- return CONVERTIBLE_TYPES;
- }
-
- @Override
- public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) {
-
- if (source == null) {
- return null;
- }
-
- // if the target type is an AggregateReference we are going to assume it is of the correct type,
- // because it was already converted.
- Class> objectType = targetDescriptor.getObjectType();
- if (objectType.isAssignableFrom(AggregateReference.class)) {
- return source;
- }
-
- Object id = ((AggregateReference, ?>) source).getId();
-
- if (id == null) {
- throw new IllegalStateException(
- String.format("Aggregate references id must not be null when converting to %s from %s to %s", source,
- sourceDescriptor, targetDescriptor));
- }
-
- return delegate.convert(id, TypeDescriptor.valueOf(id.getClass()), targetDescriptor);
- }
- }
-
- /**
- * Convert any simple type to an {@link AggregateReference}. If the {@literal targetDescriptor} contains information
- * about the generic type id will properly get converted to the desired type by the delegate
- * {@link ConversionService}.
- */
- @ReadingConverter
- private static class SimpleTypeToAggregateReferenceConverter implements GenericConverter {
-
- private static final Set CONVERTIBLE_TYPES = Collections
- .singleton(new ConvertiblePair(Object.class, AggregateReference.class));
-
- private final ConversionService delegate;
-
- SimpleTypeToAggregateReferenceConverter(ConversionService delegate) {
- this.delegate = delegate;
- }
-
- @Override
- public Set getConvertibleTypes() {
- return CONVERTIBLE_TYPES;
- }
-
- @Override
- public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) {
-
- if (source == null) {
- return null;
- }
-
- ResolvableType componentType = targetDescriptor.getResolvableType().getGenerics()[1];
- TypeDescriptor targetType = TypeDescriptor.valueOf(componentType.resolve());
- Object convertedId = delegate.convert(source, TypeDescriptor.valueOf(source.getClass()), targetType);
-
- return AggregateReference.to(convertedId);
- }
- }
-}
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..8585e99fc5 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
@@ -27,9 +27,8 @@
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.context.ApplicationContextAware;
-import org.springframework.core.convert.ConverterNotFoundException;
import org.springframework.core.convert.converter.Converter;
-import org.springframework.core.convert.converter.ConverterRegistry;
+import org.springframework.dao.NonTransientDataAccessException;
import org.springframework.data.convert.CustomConversions;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
@@ -80,7 +79,7 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
* {@link #MappingJdbcConverter(RelationalMappingContext, RelationResolver, CustomConversions, JdbcTypeFactory)}
* (MappingContext, RelationResolver, JdbcTypeFactory)} to convert arrays and large objects into JDBC-specific types.
*
- * @param context must not be {@literal null}.
+ * @param context must not be {@literal null}.
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
*/
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver) {
@@ -91,19 +90,17 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r
this.typeFactory = JdbcTypeFactory.unsupported();
this.relationResolver = relationResolver;
-
- registerAggregateReferenceConverters();
}
/**
* Creates a new {@link MappingJdbcConverter} given {@link MappingContext}.
*
- * @param context must not be {@literal null}.
+ * @param context must not be {@literal null}.
* @param relationResolver used to fetch additional relations from the database. Must not be {@literal null}.
- * @param typeFactory must not be {@literal null}
+ * @param typeFactory must not be {@literal null}
*/
public MappingJdbcConverter(RelationalMappingContext context, RelationResolver relationResolver,
- CustomConversions conversions, JdbcTypeFactory typeFactory) {
+ CustomConversions conversions, JdbcTypeFactory typeFactory) {
super(context, conversions);
@@ -112,14 +109,6 @@ public MappingJdbcConverter(RelationalMappingContext context, RelationResolver r
this.typeFactory = typeFactory;
this.relationResolver = relationResolver;
-
- registerAggregateReferenceConverters();
- }
-
- private void registerAggregateReferenceConverters() {
-
- ConverterRegistry registry = (ConverterRegistry) getConversionService();
- AggregateReferenceConverters.getConvertersToRegister(getConversionService()).forEach(registry::addConverter);
}
@Nullable
@@ -184,34 +173,78 @@ private Class> doGetColumnType(RelationalPersistentProperty property) {
return componentColumnType;
}
+ /**
+ * Read and convert a single value that is coming from a database to the {@literal targetType} expected by the domain
+ * model.
+ *
+ * @param value a value as it is returned by the driver accessing the persistence store. May be {@code null}.
+ * @param targetType {@link TypeInformation} into which the value is to be converted. Must not be {@code null}.
+ * @return
+ */
@Override
@Nullable
- public Object readValue(@Nullable Object value, TypeInformation> type) {
+ public Object readValue(@Nullable Object value, TypeInformation> targetType) {
- if (value == null) {
- return value;
+ if (null == value) {
+ return null;
}
+ TypeInformation> originalTargetType = targetType;
+ value = readJdbcArray(value);
+ targetType = determineNestedTargetType(targetType);
+
+ return readToAggregateReference(getPotentiallyConvertedSimpleRead(value, targetType), originalTargetType);
+ }
+
+ /**
+ * Unwrap a Jdbc array, if such a value is provided
+ */
+ private Object readJdbcArray(Object value) {
+
if (value instanceof Array array) {
try {
- return super.readValue(array.getArray(), type);
- } catch (SQLException | ConverterNotFoundException e) {
- LOG.info("Failed to extract a value of type %s from an Array; Attempting to use standard conversions", e);
+ return array.getArray();
+ } catch (SQLException e) {
+ throw new FailedToAccessJdbcArrayException(e);
}
}
- return super.readValue(value, type);
+ return value;
+ }
+
+ /**
+ * Determine the id type of an {@link AggregateReference} that the rest of the conversion infrastructure needs to use
+ * as a conversion target.
+ */
+ private TypeInformation> determineNestedTargetType(TypeInformation> ultimateTargetType) {
+
+ if (AggregateReference.class.isAssignableFrom(ultimateTargetType.getType())) {
+ // the id type of a AggregateReference
+ return ultimateTargetType.getTypeArguments().get(1);
+ }
+ return ultimateTargetType;
+ }
+
+ /**
+ * Convert value to an {@link AggregateReference} if that is specified by the parameter targetType.
+ */
+ private Object readToAggregateReference(Object value, TypeInformation> targetType) {
+
+ if (AggregateReference.class.isAssignableFrom(targetType.getType())) {
+ return AggregateReference.to(value);
+ }
+ return value;
}
- @Override
@Nullable
- public Object writeValue(@Nullable Object value, TypeInformation> type) {
+ @Override
+ protected Object getPotentiallyConvertedSimpleWrite(Object value, TypeInformation> type) {
- if (value == null) {
- return null;
+ if (value instanceof AggregateReference, ?> aggregateReference) {
+ return writeValue(aggregateReference.getId(), type);
}
- return super.writeValue(value, type);
+ return super.getPotentiallyConvertedSimpleWrite(value, type);
}
private boolean canWriteAsJdbcValue(@Nullable Object value) {
@@ -285,7 +318,7 @@ public R readAndResolve(TypeInformation type, RowDocument source, Identif
@Override
protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor documentAccessor,
- ValueExpressionEvaluator evaluator, ConversionContext context) {
+ ValueExpressionEvaluator evaluator, ConversionContext context) {
if (context instanceof ResolvingConversionContext rcc) {
@@ -298,6 +331,12 @@ protected RelationalPropertyValueProvider newValueProvider(RowDocumentAccessor d
return super.newValueProvider(documentAccessor, evaluator, context);
}
+ private static class FailedToAccessJdbcArrayException extends NonTransientDataAccessException {
+ public FailedToAccessJdbcArrayException(SQLException e) {
+ super("Failed to read array", e);
+ }
+ }
+
/**
* {@link RelationalPropertyValueProvider} using a resolving context to lookup relations. This is highly
* context-sensitive. Note that the identifier is held here because of a chicken and egg problem, while
@@ -314,7 +353,7 @@ class ResolvingRelationalPropertyValueProvider implements RelationalPropertyValu
private final Identifier identifier;
private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider delegate, RowDocumentAccessor accessor,
- ResolvingConversionContext context, Identifier identifier) {
+ ResolvingConversionContext context, Identifier identifier) {
AggregatePath path = context.aggregatePath();
@@ -323,7 +362,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
this.context = context;
this.identifier = path.isEntity()
? potentiallyAppendIdentifier(identifier, path.getRequiredLeafEntity(),
- property -> delegate.getValue(path.append(property)))
+ property -> delegate.getValue(path.append(property)))
: identifier;
}
@@ -331,7 +370,7 @@ private ResolvingRelationalPropertyValueProvider(AggregatePathValueProvider dele
* Conditionally append the identifier if the entity has an identifier property.
*/
static Identifier potentiallyAppendIdentifier(Identifier base, RelationalPersistentEntity> entity,
- Function getter) {
+ Function getter) {
if (entity.hasIdProperty()) {
@@ -460,7 +499,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
return context == this.context ? this
: new ResolvingRelationalPropertyValueProvider(delegate.withContext(context), accessor,
- (ResolvingConversionContext) context, identifier);
+ (ResolvingConversionContext) context, identifier);
}
}
@@ -472,7 +511,7 @@ public RelationalPropertyValueProvider withContext(ConversionContext context) {
* @param identifier
*/
private record ResolvingConversionContext(ConversionContext delegate, AggregatePath aggregatePath,
- Identifier identifier) implements ConversionContext {
+ Identifier identifier) implements ConversionContext {
@Override
public S convert(Object source, TypeInformation extends S> typeHint) {
diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java
deleted file mode 100644
index eab84c6a76..0000000000
--- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright 2021-2025 the original author or authors.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * https://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.springframework.data.jdbc.core.convert;
-
-import static org.assertj.core.api.Assertions.*;
-
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-
-import org.springframework.core.ResolvableType;
-import org.springframework.core.convert.TypeDescriptor;
-import org.springframework.core.convert.support.ConfigurableConversionService;
-import org.springframework.core.convert.support.DefaultConversionService;
-import org.springframework.data.jdbc.core.mapping.AggregateReference;
-
-/**
- * Tests for converters from an to {@link org.springframework.data.jdbc.core.mapping.AggregateReference}.
- *
- * @author Jens Schauder
- * @author Mark Paluch
- */
-class AggregateReferenceConvertersUnitTests {
-
- ConfigurableConversionService conversionService;
-
- @BeforeEach
- void setUp() {
- conversionService = new DefaultConversionService();
- AggregateReferenceConverters.getConvertersToRegister(DefaultConversionService.getSharedInstance())
- .forEach(it -> conversionService.addConverter(it));
- }
-
- @Test // GH-992
- void convertsFromSimpleValue() {
-
- ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class,
- String.class, Integer.class);
- Object converted = conversionService.convert(23, TypeDescriptor.forObject(23),
- new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
-
- assertThat(converted).isEqualTo(AggregateReference.to(23));
- }
-
- @Test // GH-992
- void convertsFromSimpleValueThatNeedsSeparateConversion() {
-
- ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class,
- String.class, Long.class);
- Object converted = conversionService.convert(23, TypeDescriptor.forObject(23),
- new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
-
- assertThat(converted).isEqualTo(AggregateReference.to(23L));
- }
-
- @Test // GH-992
- void convertsFromSimpleValueWithMissingTypeInformation() {
-
- Object converted = conversionService.convert(23, TypeDescriptor.forObject(23),
- TypeDescriptor.valueOf(AggregateReference.class));
-
- assertThat(converted).isEqualTo(AggregateReference.to(23));
- }
-
- @Test // GH-992
- void convertsToSimpleValue() {
-
- AggregateReference