Browse Source

Support AggregateReferences with custom id type.

Restructured reading conversion process into:
- converting technology base types (JDBC Arrays).
- standard and custom conversions.
- module specific conversions (AggregateReference).

Closes #1828
Original pull request #2062
pull/2076/merge
Jens Schauder 1 year ago committed by Mark Paluch
parent
commit
002acbc422
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 136
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java
  2. 118
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java
  3. 99
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java
  4. 6
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterAggregateReferenceUnitTests.java
  5. 160
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterUnitTests.java
  6. 63
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java
  7. 14
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests-hsql.sql
  8. 9
      spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTests.java
  9. 92
      spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java

136
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java

@ -1,136 +0,0 @@ @@ -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<GenericConverter> 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<ConvertiblePair> CONVERTIBLE_TYPES = Collections
.singleton(new ConvertiblePair(AggregateReference.class, Object.class));
private final ConversionService delegate;
AggregateReferenceToSimpleTypeConverter(ConversionService delegate) {
this.delegate = delegate;
}
@Override
public Set<ConvertiblePair> 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<ConvertiblePair> CONVERTIBLE_TYPES = Collections
.singleton(new ConvertiblePair(Object.class, AggregateReference.class));
private final ConversionService delegate;
SimpleTypeToAggregateReferenceConverter(ConversionService delegate) {
this.delegate = delegate;
}
@Override
public Set<ConvertiblePair> 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);
}
}
}

118
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverter.java

@ -27,9 +27,8 @@ import java.util.function.Function; @@ -27,9 +27,8 @@ import java.util.function.Function;
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;
@ -91,8 +90,6 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @@ -91,8 +90,6 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
this.typeFactory = JdbcTypeFactory.unsupported();
this.relationResolver = relationResolver;
registerAggregateReferenceConverters();
}
/**
@ -112,14 +109,6 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @@ -112,14 +109,6 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
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 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @@ -184,34 +173,78 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
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 possiblyReadToAggregateReference(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 possiblyReadToAggregateReference(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) {
@ -244,28 +277,37 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @@ -244,28 +277,37 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
public JdbcValue writeJdbcValue(@Nullable Object value, TypeInformation<?> columnType, SQLType sqlType) {
TypeInformation<?> targetType = canWriteAsJdbcValue(value) ? TypeInformation.of(JdbcValue.class) : columnType;
if (value instanceof AggregateReference<?, ?> aggregateReference) {
return writeJdbcValue(aggregateReference.getId(), columnType, sqlType);
}
Object convertedValue = writeValue(value, targetType);
if (convertedValue instanceof JdbcValue result) {
return result;
}
if (convertedValue == null || !convertedValue.getClass().isArray()) {
return JdbcValue.of(convertedValue, sqlType);
if (convertedValue == null) {
return JdbcValue.of(null, sqlType);
}
Class<?> componentType = convertedValue.getClass().getComponentType();
if (componentType != byte.class && componentType != Byte.class) {
if (convertedValue.getClass().isArray()) {// array conversion
Class<?> componentType = convertedValue.getClass().getComponentType();
if (componentType != byte.class && componentType != Byte.class) {
Object[] objectArray = requireObjectArray(convertedValue);
return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY);
}
Object[] objectArray = requireObjectArray(convertedValue);
return JdbcValue.of(typeFactory.createArray(objectArray), JDBCType.ARRAY);
}
if (componentType == Byte.class) {
convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue);
}
if (componentType == Byte.class) {
convertedValue = ArrayUtils.toPrimitive((Byte[]) convertedValue);
}
return JdbcValue.of(convertedValue, JDBCType.BINARY);
return JdbcValue.of(convertedValue, JDBCType.BINARY);
}
return JdbcValue.of(convertedValue, sqlType);
}
@SuppressWarnings("unchecked")
@ -298,6 +340,12 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements @@ -298,6 +340,12 @@ public class MappingJdbcConverter extends MappingRelationalConverter implements
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

99
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java

@ -1,99 +0,0 @@ @@ -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<Object, Integer> source = AggregateReference.to(23);
Object converted = conversionService.convert(source, TypeDescriptor.forObject(source),
TypeDescriptor.valueOf(Integer.class));
assertThat(converted).isEqualTo(23);
}
@Test // GH-992
void convertsToSimpleValueThatNeedsSeparateConversion() {
AggregateReference<Object, Integer> source = AggregateReference.to(23);
Object converted = conversionService.convert(source, TypeDescriptor.forObject(source),
TypeDescriptor.valueOf(Long.class));
assertThat(converted).isEqualTo(23L);
}
}

6
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/BasicRelationalConverterAggregateReferenceUnitTests.java → spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterAggregateReferenceUnitTests.java

@ -32,7 +32,7 @@ import org.springframework.data.util.TypeInformation; @@ -32,7 +32,7 @@ import org.springframework.data.util.TypeInformation;
*
* @author Jens Schauder
*/
public class BasicRelationalConverterAggregateReferenceUnitTests {
class MappingJdbcConverterAggregateReferenceUnitTests {
JdbcMappingContext context = new JdbcMappingContext();
JdbcConverter converter = new MappingJdbcConverter(context, mock(RelationResolver.class));
@ -40,7 +40,7 @@ public class BasicRelationalConverterAggregateReferenceUnitTests { @@ -40,7 +40,7 @@ public class BasicRelationalConverterAggregateReferenceUnitTests {
RelationalPersistentEntity<?> entity = context.getRequiredPersistentEntity(DummyEntity.class);
@Test // DATAJDBC-221
public void convertsToAggregateReference() {
void convertsToAggregateReference() {
final RelationalPersistentProperty property = entity.getRequiredPersistentProperty("reference");
@ -51,7 +51,7 @@ public class BasicRelationalConverterAggregateReferenceUnitTests { @@ -51,7 +51,7 @@ public class BasicRelationalConverterAggregateReferenceUnitTests {
}
@Test // DATAJDBC-221
public void convertsFromAggregateReference() {
void convertsFromAggregateReference() {
final RelationalPersistentProperty property = entity.getRequiredPersistentProperty("reference");

160
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/MappingJdbcConverterUnitTests.java

@ -40,6 +40,7 @@ import org.assertj.core.api.SoftAssertions; @@ -40,6 +40,7 @@ import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.core.mapping.JdbcMappingContext;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
@ -60,8 +61,7 @@ class MappingJdbcConverterUnitTests { @@ -60,8 +61,7 @@ class MappingJdbcConverterUnitTests {
private static final UUID UUID = java.util.UUID.fromString("87a48aa8-a071-705e-54a9-e52fe3a012f1");
private static final byte[] BYTES_REPRESENTING_UUID = { -121, -92, -118, -88, -96, 113, 112, 94, 84, -87, -27, 47,
-29,
-96, 18, -15 };
-29, -96, 18, -15 };
private JdbcMappingContext context = new JdbcMappingContext();
private StubbedJdbcTypeFactory typeFactory = new StubbedJdbcTypeFactory();
@ -70,7 +70,7 @@ class MappingJdbcConverterUnitTests { @@ -70,7 +70,7 @@ class MappingJdbcConverterUnitTests {
(identifier, path) -> {
throw new UnsupportedOperationException();
}, //
new JdbcCustomConversions(), //
new JdbcCustomConversions(List.of(CustomIdToLong.INSTANCE)), //
typeFactory //
);
@ -91,6 +91,7 @@ class MappingJdbcConverterUnitTests { @@ -91,6 +91,7 @@ class MappingJdbcConverterUnitTests {
checkTargetType(softly, entity, "date", Date.class);
checkTargetType(softly, entity, "timestamp", Timestamp.class);
checkTargetType(softly, entity, "uuid", UUID.class);
checkTargetType(softly, entity, "reference", Long.class);
softly.assertAll();
}
@ -216,117 +217,26 @@ class MappingJdbcConverterUnitTests { @@ -216,117 +217,26 @@ class MappingJdbcConverterUnitTests {
}
@SuppressWarnings("unused")
private static class DummyEntity {
@Id private final Long id;
private final SomeEnum someEnum;
private final LocalDateTime localDateTime;
private final LocalDate localDate;
private final LocalTime localTime;
private final ZonedDateTime zonedDateTime;
private final OffsetDateTime offsetDateTime;
private final Instant instant;
private final Date date;
private final Timestamp timestamp;
private final AggregateReference<DummyEntity, Long> reference;
private final UUID uuid;
private final AggregateReference<ReferencedByUuid, UUID> uuidRef;
private final Optional<UUID> optionalUuid;
// DATAJDBC-259
private final List<String> listOfString;
private final String[] arrayOfString;
private final List<OtherEntity> listOfEntity;
private final OtherEntity[] arrayOfEntity;
private DummyEntity(Long id, SomeEnum someEnum, LocalDateTime localDateTime, LocalDate localDate,
LocalTime localTime, ZonedDateTime zonedDateTime, OffsetDateTime offsetDateTime, Instant instant, Date date,
Timestamp timestamp, AggregateReference<DummyEntity, Long> reference, UUID uuid,
AggregateReference<ReferencedByUuid, UUID> uuidRef, Optional<java.util.UUID> optionalUUID, List<String> listOfString, String[] arrayOfString,
List<OtherEntity> listOfEntity, OtherEntity[] arrayOfEntity) {
this.id = id;
this.someEnum = someEnum;
this.localDateTime = localDateTime;
this.localDate = localDate;
this.localTime = localTime;
this.zonedDateTime = zonedDateTime;
this.offsetDateTime = offsetDateTime;
this.instant = instant;
this.date = date;
this.timestamp = timestamp;
this.reference = reference;
this.uuid = uuid;
this.uuidRef = uuidRef;
this.optionalUuid = optionalUUID;
this.listOfString = listOfString;
this.arrayOfString = arrayOfString;
this.listOfEntity = listOfEntity;
this.arrayOfEntity = arrayOfEntity;
}
public Long getId() {
return this.id;
}
public SomeEnum getSomeEnum() {
return this.someEnum;
}
public LocalDateTime getLocalDateTime() {
return this.localDateTime;
}
public LocalDate getLocalDate() {
return this.localDate;
}
public LocalTime getLocalTime() {
return this.localTime;
}
public ZonedDateTime getZonedDateTime() {
return this.zonedDateTime;
}
public OffsetDateTime getOffsetDateTime() {
return this.offsetDateTime;
}
public Instant getInstant() {
return this.instant;
}
public Date getDate() {
return this.date;
}
public Timestamp getTimestamp() {
return this.timestamp;
}
public AggregateReference<DummyEntity, Long> getReference() {
return this.reference;
}
public UUID getUuid() {
return this.uuid;
}
public List<String> getListOfString() {
return this.listOfString;
}
public String[] getArrayOfString() {
return this.arrayOfString;
}
public List<OtherEntity> getListOfEntity() {
return this.listOfEntity;
}
public OtherEntity[] getArrayOfEntity() {
return this.arrayOfEntity;
}
private record DummyEntity( //
@Id Long id, //
SomeEnum someEnum, //
LocalDateTime localDateTime, //
LocalDate localDate, //
LocalTime localTime, //
ZonedDateTime zonedDateTime, //
OffsetDateTime offsetDateTime, //
Instant instant, //
Date date, //
Timestamp timestamp, //
AggregateReference<DummyEntity, Long> reference, //
UUID uuid, //
AggregateReference<ReferencedByUuid, UUID> uuidRef, //
Optional<UUID> optionalUuid, //
List<String> listOfString, //
String[] arrayOfString, //
List<OtherEntity> listOfEntity, //
OtherEntity[] arrayOfEntity //
) {
}
@SuppressWarnings("unused")
@ -337,6 +247,18 @@ class MappingJdbcConverterUnitTests { @@ -337,6 +247,18 @@ class MappingJdbcConverterUnitTests {
@SuppressWarnings("unused")
private static class OtherEntity {}
private static class EnumIdEntity {
@Id SomeEnum id;
}
private static class CustomIdEntity {
@Id CustomId id;
}
private record CustomId(Long id) {
}
private static class StubbedJdbcTypeFactory implements JdbcTypeFactory {
Object[] arraySource;
@ -366,4 +288,14 @@ class MappingJdbcConverterUnitTests { @@ -366,4 +288,14 @@ class MappingJdbcConverterUnitTests {
return new UUID(high, low);
}
}
@WritingConverter
enum CustomIdToLong implements Converter<CustomId, Long> {
INSTANCE;
@Override
public Long convert(CustomId source) {
return source.id;
}
}
}

63
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java

@ -15,15 +15,22 @@ @@ -15,15 +15,22 @@
*/
package org.springframework.data.jdbc.repository;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.annotation.Id;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.testing.DatabaseType;
@ -52,13 +59,19 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests { @@ -52,13 +59,19 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(value = Ones.class, type = FilterType.ASSIGNABLE_TYPE))
includeFilters = @ComponentScan.Filter(value = { Ones.class, ReferencingAggregateRepository.class },
type = FilterType.ASSIGNABLE_TYPE))
static class Config {
@Bean
JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(asList(AggregateIdToLong.INSTANCE, NumberToAggregateId.INSTANCE));
}
}
@Autowired NamedParameterJdbcTemplate template;
@Autowired Ones ones;
@Autowired ReferencingAggregateRepository referencingAggregates;
@Autowired RelationalMappingContext context;
@SuppressWarnings("ConstantConditions")
@ -95,6 +108,18 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests { @@ -95,6 +108,18 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
).isEqualTo(1);
}
@Test // GH-1828
public void savesAndReadWithConvertableId() {
AggregateReference<AggregateWithConvertableId, AggregateId> idReference = AggregateReference
.to(new AggregateId(TWO_ID));
ReferencingAggregate reference = referencingAggregates
.save(new ReferencingAggregate(null, "Reference", idReference));
ReferencingAggregate reloaded = referencingAggregates.findById(reference.id).get();
assertThat(reloaded.ref()).isEqualTo(idReference);
}
interface Ones extends CrudRepository<AggregateOne, Long> {}
static class AggregateOne {
@ -109,4 +134,40 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests { @@ -109,4 +134,40 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
@Id Long id;
String name;
}
interface ReferencingAggregateRepository extends CrudRepository<ReferencingAggregate, Long> {
}
record AggregateWithConvertableId(@Id AggregateId id, String name) {
}
record AggregateId(Long value) {
}
record ReferencingAggregate(@Id Long id, String name,
AggregateReference<AggregateWithConvertableId, AggregateId> ref) {
}
@WritingConverter
enum AggregateIdToLong implements Converter<AggregateId, Long> {
INSTANCE;
@Override
public Long convert(AggregateId source) {
return source.value;
}
}
@ReadingConverter
enum NumberToAggregateId implements Converter<Number, AggregateId> {
INSTANCE;
@Override
public AggregateId convert(Number source) {
return new AggregateId(source.longValue());
}
}
}

14
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests-hsql.sql

@ -1 +1,13 @@ @@ -1 +1,13 @@
CREATE TABLE aggregate_one ( id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100), two INTEGER);
CREATE TABLE aggregate_one
(
id BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100),
two INTEGER
);
CREATE TABLE REFERENCING_AGGREGATE
(
ID BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY,
NAME VARCHAR(100),
REF INTEGER
);

9
spring-data-r2dbc/src/test/java/org/springframework/data/r2dbc/core/ReactiveDataAccessStrategyTests.java

@ -21,8 +21,8 @@ import static org.springframework.data.r2dbc.testing.Assertions.*; @@ -21,8 +21,8 @@ import static org.springframework.data.r2dbc.testing.Assertions.*;
import java.util.Arrays;
import java.util.UUID;
import org.assertj.core.api.SoftAssertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
@ -50,8 +50,11 @@ public class ReactiveDataAccessStrategyTests { @@ -50,8 +50,11 @@ public class ReactiveDataAccessStrategyTests {
UUID value = UUID.randomUUID();
assertThat(strategy.getBindValue(Parameter.from(value))).isEqualTo(Parameter.from(value.toString()));
assertThat(strategy.getBindValue(Parameter.from(Condition.New))).isEqualTo(Parameter.from("New"));
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(strategy.getBindValue(Parameter.from(value))).isEqualTo(Parameter.from(value.toString()));
softly.assertThat(strategy.getBindValue(Parameter.from(Condition.New))).isEqualTo(Parameter.from("New"));
});
}
@Test // gh-305

92
spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/MappingRelationalConverter.java

@ -44,16 +44,7 @@ import org.springframework.data.mapping.PersistentProperty; @@ -44,16 +44,7 @@ import org.springframework.data.mapping.PersistentProperty;
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;
@ -568,7 +559,9 @@ public class MappingRelationalConverter extends AbstractRelationalConverter @@ -568,7 +559,9 @@ public class MappingRelationalConverter extends AbstractRelationalConverter
continue;
}
accessor.setProperty(property, valueProviderToUse.getPropertyValue(property));
Object propertyValue = valueProviderToUse.getPropertyValue(property);
propertyValue = readValue(propertyValue, property.getTypeInformation());
accessor.setProperty(property, propertyValue);
}
}
@ -619,34 +612,23 @@ public class MappingRelationalConverter extends AbstractRelationalConverter @@ -619,34 +612,23 @@ public class MappingRelationalConverter extends AbstractRelationalConverter
return false;
}
@Override
@Nullable
public Object readValue(@Nullable Object value, TypeInformation<?> type) {
if (null == value) {
return null;
}
return getPotentiallyConvertedSimpleRead(value, type);
}
/**
* Checks whether we have a custom conversion registered for the given value into an arbitrary simple JDBC type.
* Returns the converted value if so. If not, we perform special enum handling or simply return the value as is.
* Read and convert a single value that is coming from a database to the {@literal targetType} expected by the domain
* model.
*
* @param value to be converted. Must not be {@code null}.
* @return the converted value if a conversion applies or the original value. Might return {@code null}.
* @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
private Object getPotentiallyConvertedSimpleWrite(Object value) {
Optional<Class<?>> customTarget = getConversions().getCustomWriteTarget(value.getClass());
public Object readValue(@Nullable Object value, TypeInformation<?> targetType) {
if (customTarget.isPresent()) {
return getConversionService().convert(value, customTarget.get());
if (null == value) {
return null;
}
return Enum.class.isAssignableFrom(value.getClass()) ? ((Enum<?>) value).name() : value;
return getPotentiallyConvertedSimpleRead(value, targetType);
}
/**
@ -696,33 +678,28 @@ public class MappingRelationalConverter extends AbstractRelationalConverter @@ -696,33 +678,28 @@ public class MappingRelationalConverter extends AbstractRelationalConverter
return null;
}
if (getConversions().isSimpleType(value.getClass())) {
Optional<Class<?>> customWriteTarget = getConversions().hasCustomWriteTarget(value.getClass(), type.getType())
? getConversions().getCustomWriteTarget(value.getClass(), type.getType())
: getConversions().getCustomWriteTarget(type.getType());
// custom conversion
Optional<Class<?>> customWriteTarget = determinCustomWriteTarget(value, type);
if (customWriteTarget.isPresent()) {
return getConversionService().convert(value, customWriteTarget.get());
}
if (customWriteTarget.isPresent()) {
return getConversionService().convert(value, customWriteTarget.get());
}
if (!TypeInformation.OBJECT.equals(type)) {
return getPotentiallyConvertedSimpleWrite(value, type);
}
if (type.getType().isAssignableFrom(value.getClass())) {
private Optional<Class<?>> determinCustomWriteTarget(Object value, TypeInformation<?> type) {
if (value.getClass().isEnum()) {
return getPotentiallyConvertedSimpleWrite(value);
}
return getConversions().getCustomWriteTarget(value.getClass(), type.getType())
.or(() -> getConversions().getCustomWriteTarget(type.getType()))
.or(() -> getConversions().getCustomWriteTarget(value.getClass()));
}
return value;
} else {
if (getConversionService().canConvert(value.getClass(), type.getType())) {
value = getConversionService().convert(value, type.getType());
}
}
}
@Nullable
protected Object getPotentiallyConvertedSimpleWrite(Object value, TypeInformation<?> type) {
return getPotentiallyConvertedSimpleWrite(value);
if (value instanceof Enum<?> enumValue) {
return enumValue.name();
}
if (value.getClass().isArray()) {
@ -744,9 +721,10 @@ public class MappingRelationalConverter extends AbstractRelationalConverter @@ -744,9 +721,10 @@ public class MappingRelationalConverter extends AbstractRelationalConverter
}
}
return
getConversionService().convert(value, type.getType());
if (getConversionService().canConvert(value.getClass(), type.getType())) {
return getConversionService().convert(value, type.getType());
}
return value;
}
private Object writeArray(Object value, TypeInformation<?> type) {
@ -1187,7 +1165,7 @@ public class MappingRelationalConverter extends AbstractRelationalConverter @@ -1187,7 +1165,7 @@ public class MappingRelationalConverter extends AbstractRelationalConverter
return null;
}
return context.convert(value, path.getRequiredLeafProperty().getTypeInformation());
return value;
}
@Override

Loading…
Cancel
Save