Skip to content

Commit 72fa300

Browse files
committed
Support AggregateReference in query derivation.
For a property of type AggregateReference one may provide an aggregate an AggregateReference or the id of the aggregate. Closes #987
1 parent 887f17b commit 72fa300

17 files changed

+266
-94
lines changed

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConverters.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import org.springframework.data.convert.WritingConverter;
2727
import org.springframework.data.jdbc.core.mapping.AggregateReference;
2828
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
2930

3031
/**
3132
* Converters for aggregate references. They need a {@link ConversionService} in order to delegate the conversion of the
@@ -35,6 +36,7 @@
3536
* @since 2.6
3637
*/
3738
final class AggregateReferenceConverters {
39+
3840
/**
3941
* Prevent instantiation.
4042
*/
@@ -65,7 +67,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor,
6567
return null;
6668
}
6769

68-
// if the target type is an AggregateReference we just going to assume it is of the correct type,
70+
// if the target type is an AggregateReference we are going to assume it is of the correct type,
6971
// because it was already converted.
7072
Class<?> objectType = targetDescriptor.getObjectType();
7173
if (objectType.isAssignableFrom(AggregateReference.class)) {
@@ -110,7 +112,6 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor,
110112
return null;
111113
}
112114

113-
// TODO check
114115
ResolvableType componentType = targetDescriptor.getResolvableType().getGenerics()[1];
115116
TypeDescriptor targetType = TypeDescriptor.valueOf(componentType.resolve());
116117
Object convertedId = delegate.convert(source, TypeDescriptor.valueOf(source.getClass()), targetType);

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/BasicJdbcConverter.java

+2-17
Original file line numberDiff line numberDiff line change
@@ -221,16 +221,8 @@ public Object readValue(@Nullable Object value, TypeInformation<?> type) {
221221
return value;
222222
}
223223

224-
if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) {
225-
226-
TypeDescriptor sourceDescriptor = TypeDescriptor.valueOf(value.getClass());
227-
TypeDescriptor targetDescriptor = typeInformationToTypeDescriptor(type);
228-
229-
return getConversionService().convert(value, sourceDescriptor,
230-
targetDescriptor);
231-
}
232-
233-
if (value instanceof Array) {
224+
if ( !getConversions().hasCustomReadTarget(value.getClass(), type.getType()) &&
225+
value instanceof Array) {
234226
try {
235227
return readValue(((Array) value).getArray(), type);
236228
} catch (SQLException | ConverterNotFoundException e) {
@@ -241,13 +233,6 @@ public Object readValue(@Nullable Object value, TypeInformation<?> type) {
241233
return super.readValue(value, type);
242234
}
243235

244-
private static TypeDescriptor typeInformationToTypeDescriptor(TypeInformation<?> type) {
245-
246-
Class<?>[] generics = type.getTypeArguments().stream().map(TypeInformation::getType).toArray(Class[]::new);
247-
248-
return new TypeDescriptor(ResolvableType.forClassWithGenerics(type.getType(), generics), null, null);
249-
}
250-
251236
/*
252237
* (non-Javadoc)
253238
* @see org.springframework.data.relational.core.conversion.RelationalConverter#writeValue(java.lang.Object, org.springframework.data.util.TypeInformation)

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/core/convert/JdbcCustomConversions.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
import java.util.List;
2222

2323
import org.springframework.core.convert.ConversionService;
24-
import org.springframework.core.convert.converter.Converter;
2524
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
2625
import org.springframework.core.convert.support.DefaultConversionService;
2726
import org.springframework.data.convert.CustomConversions;
@@ -53,6 +52,7 @@ public class JdbcCustomConversions extends CustomConversions {
5352
STORE_CONVERTERS = Collections.unmodifiableCollection(converters);
5453

5554
}
55+
5656
private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER,
5757
STORE_CONVERTERS);
5858

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

-5
Original file line numberDiff line numberDiff line change
@@ -146,11 +146,6 @@ private static void validateProperty(PersistentPropertyPathExtension path) {
146146
throw new IllegalArgumentException(
147147
String.format("Cannot query by nested entity: %s", path.getRequiredPersistentPropertyPath().toDotPath()));
148148
}
149-
150-
if (path.getRequiredPersistentPropertyPath().getLeafProperty().isReference()) {
151-
throw new IllegalArgumentException(
152-
String.format("Cannot query by reference: %s", path.getRequiredPersistentPropertyPath().toDotPath()));
153-
}
154149
}
155150

156151
/**

Diff for: spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/QueryMapper.java

+92-7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.query;
1717

18+
import java.sql.JDBCType;
1819
import java.sql.Types;
1920
import java.util.ArrayList;
2021
import java.util.Collection;
@@ -245,8 +246,8 @@ private Condition getCondition(CriteriaDefinition criteria, MapSqlParameterSourc
245246
return mapCondition(criteria, parameterSource, table, entity);
246247
}
247248

248-
private Condition combine(@Nullable Condition currentCondition,
249-
CriteriaDefinition.Combinator combinator, Condition nextCondition) {
249+
private Condition combine(@Nullable Condition currentCondition, CriteriaDefinition.Combinator combinator,
250+
Condition nextCondition) {
250251

251252
if (currentCondition == null) {
252253
currentCondition = nextCondition;
@@ -292,6 +293,17 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc
292293

293294
mappedValue = convertValue(value, propertyField.getTypeHint());
294295
sqlType = propertyField.getSqlType();
296+
297+
} else if (propertyField instanceof MetadataBackedField //
298+
&& ((MetadataBackedField) propertyField).property != null //
299+
&& (criteria.getValue() == null || !criteria.getValue().getClass().isArray())) {
300+
301+
final RelationalPersistentProperty property = ((MetadataBackedField) propertyField).property;
302+
JdbcValue jdbcValue = convertSpecial(property, criteria.getValue());
303+
mappedValue = jdbcValue.getValue();
304+
sqlType = jdbcValue.getJdbcType() != null ? jdbcValue.getJdbcType().getVendorTypeNumber()
305+
: propertyField.getSqlType();
306+
295307
} else {
296308

297309
mappedValue = convertValue(criteria.getValue(), propertyField.getTypeHint());
@@ -302,6 +314,84 @@ private Condition mapCondition(CriteriaDefinition criteria, MapSqlParameterSourc
302314
criteria.isIgnoreCase());
303315
}
304316

317+
/**
318+
* Converts values while taking special value types like arrays, {@link Iterable}, or {@link Pair}.
319+
*
320+
* @param property the property to which the value relates. It determines the type to convert to. Must not be
321+
* {@literal null}.
322+
* @param value the value to be converted.
323+
* @return a non null {@link JdbcValue} holding the converted value and the appropriate JDBC type information.
324+
*/
325+
private JdbcValue convertSpecial(RelationalPersistentProperty property, @Nullable Object value) {
326+
327+
if (value == null) {
328+
return JdbcValue.of(null, JDBCType.NULL);
329+
}
330+
331+
if (value instanceof Pair) {
332+
333+
final JdbcValue first = convertSimple(property, ((Pair<?, ?>) value).getFirst());
334+
final JdbcValue second = convertSimple(property, ((Pair<?, ?>) value).getSecond());
335+
return JdbcValue.of(Pair.of(first.getValue(), second.getValue()), first.getJdbcType());
336+
}
337+
338+
if (value instanceof Iterable) {
339+
340+
List<Object> mapped = new ArrayList<>();
341+
JDBCType jdbcType = null;
342+
343+
for (Object o : (Iterable<?>) value) {
344+
345+
final JdbcValue jdbcValue = convertSimple(property, o);
346+
if (jdbcType == null) {
347+
jdbcType = jdbcValue.getJdbcType();
348+
}
349+
350+
mapped.add(jdbcValue.getValue());
351+
}
352+
353+
return JdbcValue.of(mapped, jdbcType);
354+
}
355+
356+
if (value.getClass().isArray()) {
357+
358+
final Object[] valueAsArray = (Object[]) value;
359+
final Object[] mappedValueArray = new Object[valueAsArray.length];
360+
JDBCType jdbcType = null;
361+
362+
for (int i = 0; i < valueAsArray.length; i++) {
363+
364+
final JdbcValue jdbcValue = convertSimple(property, valueAsArray[i]);
365+
if (jdbcType == null) {
366+
jdbcType = jdbcValue.getJdbcType();
367+
}
368+
369+
mappedValueArray[i] = jdbcValue.getValue();
370+
}
371+
372+
return JdbcValue.of(mappedValueArray, jdbcType);
373+
}
374+
375+
return convertSimple(property, value);
376+
}
377+
378+
/**
379+
* Converts values to a {@link JdbcValue}.
380+
*
381+
* @param property the property to which the value relates. It determines the type to convert to. Must not be
382+
* {@literal null}.
383+
* @param value the value to be converted.
384+
* @return a non null {@link JdbcValue} holding the converted value and the appropriate JDBC type information.
385+
*/
386+
private JdbcValue convertSimple(RelationalPersistentProperty property, Object value) {
387+
388+
return converter.writeJdbcValue( //
389+
value, //
390+
converter.getColumnType(property), //
391+
converter.getSqlType(property) //
392+
);
393+
}
394+
305395
private Condition mapEmbeddedObjectCondition(CriteriaDefinition criteria, MapSqlParameterSource parameterSource,
306396
Table table, RelationalPersistentProperty embeddedProperty) {
307397

@@ -740,11 +830,6 @@ public TypeInformation<?> getTypeHint() {
740830
return this.property.getTypeInformation();
741831
}
742832

743-
if (this.property.getType().isInterface()
744-
|| (java.lang.reflect.Modifier.isAbstract(this.property.getType().getModifiers()))) {
745-
return ClassTypeInformation.OBJECT;
746-
}
747-
748833
return this.property.getTypeInformation();
749834
}
750835

Diff for: spring-data-jdbc/src/test/java/org/springframework/data/jdbc/core/convert/AggregateReferenceConvertersUnitTests.java

+21-13
Original file line numberDiff line numberDiff line change
@@ -15,46 +15,53 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18+
import static org.assertj.core.api.Assertions.*;
19+
1820
import org.junit.jupiter.api.Test;
1921
import org.springframework.core.ResolvableType;
2022
import org.springframework.core.convert.TypeDescriptor;
2123
import org.springframework.core.convert.support.DefaultConversionService;
2224
import org.springframework.data.jdbc.core.mapping.AggregateReference;
2325

24-
import static org.assertj.core.api.Assertions.*;
25-
2626
/**
2727
* Tests for converters from an to {@link org.springframework.data.jdbc.core.mapping.AggregateReference}.
2828
*
2929
* @author Jens Schauder
3030
*/
3131
class AggregateReferenceConvertersUnitTests {
3232

33-
AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter simpleToAggregate = new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(DefaultConversionService.getSharedInstance());
34-
AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter aggregateToSimple = new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(DefaultConversionService.getSharedInstance());
33+
AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter simpleToAggregate = new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(
34+
DefaultConversionService.getSharedInstance());
35+
AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter aggregateToSimple = new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(
36+
DefaultConversionService.getSharedInstance());
3537

3638
@Test // #992
3739
void convertsFromSimpleValue() {
3840

39-
ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, String.class, Integer.class);
40-
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
41+
ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class,
42+
String.class, Integer.class);
43+
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23),
44+
new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
4145

4246
assertThat(converted).isEqualTo(AggregateReference.to(23));
4347
}
4448

4549
@Test // #992
4650
void convertsFromSimpleValueThatNeedsSeparateConversion() {
4751

48-
ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class, String.class, Long.class);
49-
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
52+
ResolvableType aggregateReferenceWithIdTypeInteger = ResolvableType.forClassWithGenerics(AggregateReference.class,
53+
String.class, Long.class);
54+
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23),
55+
new TypeDescriptor(aggregateReferenceWithIdTypeInteger, null, null));
5056

5157
assertThat(converted).isEqualTo(AggregateReference.to(23L));
5258
}
5359

5460
@Test // #992
5561
void convertsFromSimpleValueWithMissingTypeInformation() {
5662

57-
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23), TypeDescriptor.valueOf(AggregateReference.class));
63+
final Object converted = simpleToAggregate.convert(23, TypeDescriptor.forObject(23),
64+
TypeDescriptor.valueOf(AggregateReference.class));
5865

5966
assertThat(converted).isEqualTo(AggregateReference.to(23));
6067
}
@@ -64,7 +71,8 @@ void convertsToSimpleValue() {
6471

6572
final AggregateReference<Object, Integer> source = AggregateReference.to(23);
6673

67-
final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(Integer.class));
74+
final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source),
75+
TypeDescriptor.valueOf(Integer.class));
6876

6977
assertThat(converted).isEqualTo(23);
7078
}
@@ -74,10 +82,10 @@ void convertsToSimpleValueThatNeedsSeparateConversion() {
7482

7583
final AggregateReference<Object, Integer> source = AggregateReference.to(23);
7684

77-
final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(Long.class));
85+
final Object converted = aggregateToSimple.convert(source, TypeDescriptor.forObject(source),
86+
TypeDescriptor.valueOf(Long.class));
7887

7988
assertThat(converted).isEqualTo(23L);
8089
}
8190

82-
83-
}
91+
}

Diff for: spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

+31
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.springframework.data.domain.PageRequest;
5151
import org.springframework.data.domain.Pageable;
5252
import org.springframework.data.domain.Slice;
53+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
5354
import org.springframework.data.jdbc.repository.query.Modifying;
5455
import org.springframework.data.jdbc.repository.query.Query;
5556
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
@@ -514,6 +515,32 @@ void derivedQueryWithBooleanLiteralFindsCorrectValues() {
514515
assertThat(result).extracting(e -> e.idProp).containsExactly(entity.idProp);
515516
}
516517

518+
@Test // #987
519+
void queryBySimpleReference() {
520+
521+
final DummyEntity one = repository.save(createDummyEntity());
522+
DummyEntity two = createDummyEntity();
523+
two.ref = AggregateReference.to(one.idProp);
524+
two = repository.save(two);
525+
526+
List<DummyEntity> result = repository.findByRef(one.idProp.intValue());
527+
528+
assertThat(result).extracting(e -> e.idProp).containsExactly(two.idProp);
529+
}
530+
531+
@Test // #987
532+
void queryByAggregateReference() {
533+
534+
final DummyEntity one = repository.save(createDummyEntity());
535+
DummyEntity two = createDummyEntity();
536+
two.ref = AggregateReference.to(one.idProp);
537+
two = repository.save(two);
538+
539+
List<DummyEntity> result = repository.findByRef(two.ref);
540+
541+
assertThat(result).extracting(e -> e.idProp).containsExactly(two.idProp);
542+
}
543+
517544
private Instant createDummyBeforeAndAfterNow() {
518545

519546
Instant now = Instant.now();
@@ -585,6 +612,9 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
585612
void updateWithIntervalCalculation(@Param("id") Long id, @Param("start") LocalDateTime start);
586613

587614
List<DummyEntity> findByFlagTrue();
615+
616+
List<DummyEntity> findByRef(int ref);
617+
List<DummyEntity> findByRef(AggregateReference<DummyEntity, Long> ref);
588618
}
589619

590620
@Configuration
@@ -637,6 +667,7 @@ static class DummyEntity {
637667
OffsetDateTime offsetDateTime;
638668
@Id private Long idProp;
639669
boolean flag;
670+
AggregateReference<DummyEntity, Long> ref;
640671

641672
public DummyEntity(String name) {
642673
this.name = name;

0 commit comments

Comments
 (0)