diff --git a/pom.xml b/pom.xml index eeaa0b9e93..cae7dc4252 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 03d6a5c2a0..004f714bc2 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 - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index af9ad0904e..3800c394dd 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-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 new file mode 100644 index 0000000000..3e6afb8589 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java @@ -0,0 +1,121 @@ +/* + * Copyright 2021 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.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 + * @since 2.6 + */ +class AggregateReferenceConverters { + + /** + * Prevent instantiation. + */ + private AggregateReferenceConverters() {} + + /** + * Converts from an AggregateReference to its id, leaving the conversion of the id to the ultimate target type to the + * delegate {@link ConversionService}. + */ + @WritingConverter + static class AggregateReferenceToSimpleTypeConverter implements GenericConverter { + + private final ConversionService delegate; + + AggregateReferenceToSimpleTypeConverter(ConversionService delegate) { + this.delegate = delegate; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(AggregateReference.class, Object.class)); + } + + @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 + static class SimpleTypeToAggregateReferenceConverter implements GenericConverter { + + private final ConversionService delegate; + + SimpleTypeToAggregateReferenceConverter(ConversionService delegate) { + this.delegate = delegate; + } + + @Override + public Set getConvertibleTypes() { + return Collections.singleton(new ConvertiblePair(Object.class, AggregateReference.class)); + } + + @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/BasicJdbcConverter.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java index 84b3fc665d..86df97b99a 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java @@ -26,7 +26,9 @@ import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConverterNotFoundException; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.AggregateReference; @@ -219,20 +221,8 @@ public Object readValue(@Nullable Object value, TypeInformation type) { return value; } - if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { - return getConversionService().convert(value, type.getType()); - } - - if (AggregateReference.class.isAssignableFrom(type.getType())) { - - if (type.getType().isAssignableFrom(value.getClass())) { - return value; - } - - return readAggregateReference(value, type); - } - - if (value instanceof Array) { + if ( !getConversions().hasCustomReadTarget(value.getClass(), type.getType()) && + value instanceof Array) { try { return readValue(((Array) value).getArray(), type); } catch (SQLException | ConverterNotFoundException e) { @@ -243,14 +233,6 @@ public Object readValue(@Nullable Object value, TypeInformation type) { return super.readValue(value, type); } - @SuppressWarnings("ConstantConditions") - private Object readAggregateReference(@Nullable Object value, TypeInformation type) { - - TypeInformation idType = type.getSuperTypeInformation(AggregateReference.class).getTypeArguments().get(1); - - return AggregateReference.to(readValue(value, idType)); - } - /* * (non-Javadoc) * @see org.springframework.data.relational.core.conversion.RelationalConverter#writeValue(java.lang.Object, org.springframework.data.util.TypeInformation) @@ -263,10 +245,6 @@ public Object writeValue(@Nullable Object value, TypeInformation type) { return null; } - if (AggregateReference.class.isAssignableFrom(value.getClass())) { - return writeValue(((AggregateReference) value).getId(), type); - } - return super.writeValue(value, type); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java index e91329b8b6..15cfbaf3b4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java @@ -15,11 +15,14 @@ */ package org.springframework.data.jdbc.core.convert; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.CustomConversions; import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes; @@ -36,8 +39,20 @@ */ public class JdbcCustomConversions extends CustomConversions { - private static final Collection STORE_CONVERTERS = Collections - .unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister()); + private static final Collection STORE_CONVERTERS; + + static { + + List converters = new ArrayList<>(Jsr310TimestampBasedConverters.getConvertersToRegister()); + + ConversionService conversionService = DefaultConversionService.getSharedInstance(); + converters.add(new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(conversionService)); + converters.add(new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(conversionService)); + + STORE_CONVERTERS = Collections.unmodifiableCollection(converters); + + } + private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER, STORE_CONVERTERS); diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 5f6105a174..d86549f141 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -146,11 +146,6 @@ private static void validateProperty(PersistentPropertyPathExtension path) { throw new IllegalArgumentException( String.format("Cannot query by nested entity: %s", path.getRequiredPersistentPropertyPath().toDotPath())); } - - if (path.getRequiredPersistentPropertyPath().getLeafProperty().isReference()) { - throw new IllegalArgumentException( - String.format("Cannot query by reference: %s", path.getRequiredPersistentPropertyPath().toDotPath())); - } } /** diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java index 328e088295..fde30f9b74 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java @@ -15,6 +15,7 @@ */ package org.springframework.data.jdbc.repository.query; +import java.sql.JDBCType; import java.sql.Types; import java.util.ArrayList; import java.util.Collection; @@ -245,8 +246,8 @@ private Condition getCondition(CriteriaDefinition criteria, MapSqlParameterSourc return mapCondition(criteria, parameterSource, table, entity); } - private Condition combine(@Nullable Condition currentCondition, - CriteriaDefinition.Combinator combinator, Condition nextCondition) { + private Condition combine(@Nullable Condition currentCondition, CriteriaDefinition.Combinator combinator, + Condition nextCondition) { if (currentCondition == null) { currentCondition = nextCondition; @@ -292,6 +293,17 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc mappedValue = convertValue(value, propertyField.getTypeHint()); sqlType = propertyField.getSqlType(); + + } else if (propertyField instanceof MetadataBackedField // + && ((MetadataBackedField) propertyField).property != null // + && (criteria.getValue() == null || !criteria.getValue().getClass().isArray())) { + + RelationalPersistentProperty property = ((MetadataBackedField) propertyField).property; + JdbcValue jdbcValue = convertSpecial(property, criteria.getValue()); + mappedValue = jdbcValue.getValue(); + sqlType = jdbcValue.getJdbcType() != null ? jdbcValue.getJdbcType().getVendorTypeNumber() + : propertyField.getSqlType(); + } else { mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint()); @@ -302,6 +314,84 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc criteria.isIgnoreCase()); } + /** + * Converts values while taking special value types like arrays, {@link Iterable}, or {@link Pair}. + * + * @param property the property to which the value relates. It determines the type to convert to. Must not be + * {@literal null}. + * @param value the value to be converted. + * @return a non null {@link JdbcValue} holding the converted value and the appropriate JDBC type information. + */ + private JdbcValue convertSpecial(RelationalPersistentProperty property, @Nullable Object value) { + + if (value == null) { + return JdbcValue.of(null, JDBCType.NULL); + } + + if (value instanceof Pair) { + + final JdbcValue first = convertSimple(property, ((Pair) value).getFirst()); + final JdbcValue second = convertSimple(property, ((Pair) value).getSecond()); + return JdbcValue.of(Pair.of(first.getValue(), second.getValue()), first.getJdbcType()); + } + + if (value instanceof Iterable) { + + List mapped = new ArrayList<>(); + JDBCType jdbcType = null; + + for (Object o : (Iterable) value) { + + final JdbcValue jdbcValue = convertSimple(property, o); + if (jdbcType == null) { + jdbcType = jdbcValue.getJdbcType(); + } + + mapped.add(jdbcValue.getValue()); + } + + return JdbcValue.of(mapped, jdbcType); + } + + if (value.getClass().isArray()) { + + final Object[] valueAsArray = (Object[]) value; + final Object[] mappedValueArray = new Object[valueAsArray.length]; + JDBCType jdbcType = null; + + for (int i = 0; i < valueAsArray.length; i++) { + + final JdbcValue jdbcValue = convertSimple(property, valueAsArray[i]); + if (jdbcType == null) { + jdbcType = jdbcValue.getJdbcType(); + } + + mappedValueArray[i] = jdbcValue.getValue(); + } + + return JdbcValue.of(mappedValueArray, jdbcType); + } + + return convertSimple(property, value); + } + + /** + * Converts values to a {@link JdbcValue}. + * + * @param property the property to which the value relates. It determines the type to convert to. Must not be + * {@literal null}. + * @param value the value to be converted. + * @return a non null {@link JdbcValue} holding the converted value and the appropriate JDBC type information. + */ + private JdbcValue convertSimple(RelationalPersistentProperty property, Object value) { + + return converter.writeJdbcValue( // + value, // + converter.getColumnType(property), // + converter.getSqlType(property) // + ); + } + private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource, Table table, RelationalPersistentProperty embeddedProperty) { @@ -740,11 +830,6 @@ public TypeInformation getTypeHint() { return this.property.getTypeInformation(); } - if (this.property.getType().isInterface() - || (java.lang.reflect.Modifier.isAbstract(this.property.getType().getModifiers()))) { - return ClassTypeInformation.OBJECT; - } - return this.property.getTypeInformation(); } 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 new file mode 100644 index 0000000000..5b00beaba6 --- /dev/null +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java @@ -0,0 +1,91 @@ +/* + * Copyright 2021 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.Test; +import org.springframework.core.ResolvableType; +import org.springframework.core.convert.TypeDescriptor; +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 + */ +class AggregateReferenceConvertersUnitTests { + + AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter simpleToAggregate = new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter( + DefaultConversionService.getSharedInstance()); + AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter aggregateToSimple = new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter( + DefaultConversionService.getSharedInstance()); + + @Test // #992 + void convertsFromSimpleValue() { + + ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, + String.class, Integer.class); + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), + new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null)); + + assertThat(converted).isEqualTo(AggregateReference.to(23)); + } + + @Test // #992 + void convertsFromSimpleValueThatNeedsSeparateConversion() { + + ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, + String.class, Long.class); + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), + new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null)); + + assertThat(converted).isEqualTo(AggregateReference.to(23L)); + } + + @Test // #992 + void convertsFromSimpleValueWithMissingTypeInformation() { + + final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), + TypeDescriptor.valueOf(AggregateReference.class)); + + assertThat(converted).isEqualTo(AggregateReference.to(23)); + } + + @Test // #992 + void convertsToSimpleValue() { + + final AggregateReference source = AggregateReference.to(23); + + final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), + TypeDescriptor.valueOf(Integer.class)); + + assertThat(converted).isEqualTo(23); + } + + @Test // #992 + void convertsToSimpleValueThatNeedsSeparateConversion() { + + final AggregateReference source = AggregateReference.to(23); + + final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), + TypeDescriptor.valueOf(Long.class)); + + assertThat(converted).isEqualTo(23L); + } + +} diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 41a4fce507..4306775b29 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -50,6 +50,7 @@ import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; +import org.springframework.data.jdbc.core.mapping.AggregateReference; import org.springframework.data.jdbc.repository.query.Modifying; import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; @@ -514,6 +515,32 @@ void derivedQueryWithBooleanLiteralFindsCorrectValues() { assertThat(result).extracting(e -> e.idProp).containsExactly(entity.idProp); } + @Test // #987 + void queryBySimpleReference() { + + final DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = createDummyEntity(); + two.ref = AggregateReference.to(one.idProp); + two = repository.save(two); + + List result = repository.findByRef(one.idProp.intValue()); + + assertThat(result).extracting(e -> e.idProp).containsExactly(two.idProp); + } + + @Test // #987 + void queryByAggregateReference() { + + final DummyEntity one = repository.save(createDummyEntity()); + DummyEntity two = createDummyEntity(); + two.ref = AggregateReference.to(one.idProp); + two = repository.save(two); + + List result = repository.findByRef(two.ref); + + assertThat(result).extracting(e -> e.idProp).containsExactly(two.idProp); + } + private Instant createDummyBeforeAndAfterNow() { Instant now = Instant.now(); @@ -585,6 +612,9 @@ interface DummyEntityRepository extends CrudRepository { void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start); List findByFlagTrue(); + + List findByRef(int ref); + List findByRef(AggregateReference ref); } @Configuration @@ -637,6 +667,7 @@ static class DummyEntity { OffsetDateTime offsetDateTime; @Id private Long idProp; boolean flag; + AggregateReference ref; public DummyEntity(String name) { this.name = name; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java index 3416ae0bfa..2bb838fd1f 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQueryUnitTests.java @@ -16,6 +16,7 @@ package org.springframework.data.jdbc.repository.query; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.SoftAssertions.*; import static org.mockito.Mockito.*; import lombok.AllArgsConstructor; @@ -27,11 +28,9 @@ import java.util.List; import java.util.Properties; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.data.annotation.Id; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter; @@ -64,7 +63,7 @@ public class PartTreeJdbcQueryUnitTests { private static final String TABLE = "\"users\""; - private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"hated\".\"USER\" AS \"HATED_USER\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; + private static final String ALL_FIELDS = "\"users\".\"ID\" AS \"ID\", \"users\".\"AGE\" AS \"AGE\", \"users\".\"ACTIVE\" AS \"ACTIVE\", \"users\".\"LAST_NAME\" AS \"LAST_NAME\", \"users\".\"FIRST_NAME\" AS \"FIRST_NAME\", \"users\".\"DATE_OF_BIRTH\" AS \"DATE_OF_BIRTH\", \"users\".\"HOBBY_REFERENCE\" AS \"HOBBY_REFERENCE\", \"hated\".\"NAME\" AS \"HATED_NAME\", \"users\".\"USER_CITY\" AS \"USER_CITY\", \"users\".\"USER_STREET\" AS \"USER_STREET\""; private static final String JOIN_CLAUSE = "FROM \"users\" LEFT OUTER JOIN \"HOBBY\" \"hated\" ON \"hated\".\"USER\" = \"users\".\"ID\""; private static final String BASE_SELECT = "SELECT " + ALL_FIELDS + " " + JOIN_CLAUSE; @@ -79,11 +78,22 @@ public void shouldFailForQueryByReference() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> createQuery(queryMethod)); } - @Test // DATAJDBC-318 - public void shouldFailForQueryByAggregateReference() throws Exception { + @Test // #922 + public void createQueryByAggregateReference() throws Exception { JdbcQueryMethod queryMethod = getQueryMethod("findAllByHobbyReference", Hobby.class); - assertThatIllegalArgumentException().isThrownBy(() -> createQuery(queryMethod)); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + final Hobby hobby = new Hobby(); + hobby.name = "twentythree"; + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + + assertSoftly(softly -> { + + softly.assertThat(query.getQuery()) + .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); + + softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + }); } @Test // DATAJDBC-318 @@ -100,11 +110,38 @@ public void shouldFailForQueryByEmbeddedList() throws Exception { assertThatIllegalArgumentException().isThrownBy(() -> createQuery(queryMethod)); } - @Test // DATAJDBC-318 - public void shouldFailForAggregateReference() throws Exception { + @Test // #922 + public void createQueryForQueryByAggregateReference() throws Exception { - JdbcQueryMethod queryMethod = getQueryMethod("findByAnotherEmbeddedList", Object.class); - assertThatIllegalArgumentException().isThrownBy(() -> createQuery(queryMethod)); + JdbcQueryMethod queryMethod = getQueryMethod("findViaReferenceByHobbyReference", AggregateReference.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + final AggregateReference hobby = AggregateReference.to("twentythree"); + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + + assertSoftly(softly -> { + + softly.assertThat(query.getQuery()) + .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); + + softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + }); + } + + @Test // #922 + public void createQueryForQueryByAggregateReferenceId() throws Exception { + + JdbcQueryMethod queryMethod = getQueryMethod("findViaIdByHobbyReference", String.class); + PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); + final String hobby = "twentythree"; + ParametrizedQuery query = jdbcQuery.createQuery(getAccessor(queryMethod, new Object[] {hobby}), returnedType); + + assertSoftly(softly -> { + + softly.assertThat(query.getQuery()) + .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"HOBBY_REFERENCE\" = :hobby_reference"); + + softly.assertThat(query.getParameterSource().getValue("hobby_reference")).isEqualTo("twentythree"); + }); } @Test // DATAJDBC-318 @@ -176,6 +213,7 @@ public void createsQueryToFindAllEntitiesByOneOfTwoStringAttributes() throws Exc + ".\"FIRST_NAME\" = :first_name)"); } + @Test // DATAJDBC-318 public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Exception { @@ -186,11 +224,14 @@ public void createsQueryToFindAllEntitiesByDateAttributeBetween() throws Excepti RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { from, to }); ParametrizedQuery query = jdbcQuery.createQuery(accessor, returnedType); - assertThat(query.getQuery()) - .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" BETWEEN :date_of_birth AND :date_of_birth1"); + assertSoftly(softly -> { + + softly.assertThat(query.getQuery()) + .isEqualTo(BASE_SELECT + " WHERE " + TABLE + ".\"DATE_OF_BIRTH\" BETWEEN :date_of_birth AND :date_of_birth1"); - assertThat(query.getParameterSource().getValue("date_of_birth")).isEqualTo(from); - assertThat(query.getParameterSource().getValue("date_of_birth1")).isEqualTo(to); + softly.assertThat(query.getParameterSource().getValue("date_of_birth")).isEqualTo(from); + softly.assertThat(query.getParameterSource().getValue("date_of_birth1")).isEqualTo(to); + }); } @Test // DATAJDBC-318 @@ -250,6 +291,7 @@ public void createsQueryToFindAllEntitiesByDateAttributeAfter() throws Exception @Test // DATAJDBC-318 public void createsQueryToFindAllEntitiesByDateAttributeBefore() throws Exception { + JdbcQueryMethod queryMethod = getQueryMethod("findAllByDateOfBirthBefore", Date.class); PartTreeJdbcQuery jdbcQuery = createQuery(queryMethod); RelationalParametersParameterAccessor accessor = getAccessor(queryMethod, new Object[] { new Date() }); @@ -608,6 +650,10 @@ interface UserRepository extends Repository { List findAllByHobbyReference(Hobby hobby); + List findViaReferenceByHobbyReference(AggregateReference hobby); + + List findViaIdByHobbyReference(String hobby); + List findAllByLastNameAndFirstName(String lastName, String firstName); List findAllByLastNameOrFirstName(String lastName, String firstName); @@ -708,6 +754,7 @@ static class AnotherEmbedded { } static class Hobby { + @Id String name; } } diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql index d41d8accdd..34be74ec51 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql @@ -2,9 +2,10 @@ DROP TABLE dummy_entity; CREATE TABLE dummy_entity ( - id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP, + id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP, OFFSET_DATE_TIME TIMESTAMP, -- with time zone is only supported with z/OS - FLAG BOOLEAN + FLAG BOOLEAN, + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql index 5a3f1654a2..b3b93bc744 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql @@ -1,8 +1,9 @@ CREATE TABLE dummy_entity ( - id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP, + id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, - FLAG BOOLEAN + FLAG BOOLEAN, + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 5a3f1654a2..b3b93bc744 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -1,8 +1,9 @@ CREATE TABLE dummy_entity ( - id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP, + id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, - FLAG BOOLEAN + FLAG BOOLEAN, + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 663446bdcc..949e626399 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -1,8 +1,9 @@ CREATE TABLE dummy_entity ( - id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3), + id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP(3), OFFSET_DATE_TIME TIMESTAMP(3), - FLAG BOOLEAN + FLAG BOOLEAN, + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index f18b9da5cc..15f8881327 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -1,9 +1,10 @@ DROP TABLE IF EXISTS dummy_entity; CREATE TABLE dummy_entity ( - id_Prop BIGINT IDENTITY PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME DATETIME, + id_Prop BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME DATETIME, OFFSET_DATE_TIME DATETIMEOFFSET, - FLAG BIT + FLAG BIT, + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 60e23ca6bd..e3baa94602 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1,10 +1,12 @@ -SET SQL_MODE='ALLOW_INVALID_DATES'; +SET +SQL_MODE='ALLOW_INVALID_DATES'; CREATE TABLE DUMMY_ENTITY ( - ID_PROP BIGINT AUTO_INCREMENT PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL, + ID_PROP BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL, OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL, - FLAG BIT(1) + FLAG BIT(1), + REF BIGINT ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql index 5a92e2a238..e71eb63286 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql @@ -2,9 +2,10 @@ DROP TABLE DUMMY_ENTITY CASCADE CONSTRAINTS PURGE; CREATE TABLE DUMMY_ENTITY ( - ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, - NAME VARCHAR2(100), - POINT_IN_TIME TIMESTAMP, + ID_PROP NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, + NAME VARCHAR2(100), + POINT_IN_TIME TIMESTAMP, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, - FLAG NUMBER(1,0) + FLAG NUMBER(1,0), + REF NUMBER ); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 05f4908e71..97fc78c9da 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -1,9 +1,10 @@ DROP TABLE dummy_entity; CREATE TABLE dummy_entity ( - id_Prop SERIAL PRIMARY KEY, - NAME VARCHAR(100), - POINT_IN_TIME TIMESTAMP, + id_Prop SERIAL PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, - FLAG BOOLEAN + FLAG BOOLEAN, + REF BIGINT ); diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 4e42a006ec..bf0984075d 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.3.0-SNAPSHOT + 2.3.0-992-agg-ref-converters-SNAPSHOT diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java index fad388e718..8d8b7daae0 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/conversion/BasicRelationalConverter.java @@ -19,7 +19,9 @@ import java.util.Optional; import java.util.function.Function; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.data.convert.CustomConversions; @@ -157,8 +159,12 @@ public Object readValue(@Nullable Object value, TypeInformation type) { return null; } - if (conversions.hasCustomReadTarget(value.getClass(), type.getType())) { - return conversionService.convert(value, type.getType()); + if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) { + + TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(value.getClass()); + TypeDescriptor targetDescriptor = typeInformationToTypeDescriptor(type); + + return getConversionService().convert(value, sourceDescriptor, targetDescriptor); } return getPotentiallyConvertedSimpleRead(value, type.getType()); @@ -246,6 +252,13 @@ private Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullab return conversionService.convert(value, target); } + protected static TypeDescriptor typeInformationToTypeDescriptor(TypeInformation type) { + + Class[] generics = type.getTypeArguments().stream().map(TypeInformation::getType).toArray(Class[]::new); + + return new TypeDescriptor(ResolvableType.forClassWithGenerics(type.getType(), generics), null, null); + } + /** * Converter-aware {@link ParameterValueProvider}. *