Skip to content

Commit c55690f

Browse files
committed
Support AggregateReference in query derivation.
We register now reading and writing AggregateReferences instead of using custom code paths. Thereby they get supported by normal handling of conversions including in query derivation. For a property of type AggregateReference one may provide an aggregate an AggregateReference or the id of the aggregate. Closes #992
1 parent 5db67b4 commit c55690f

17 files changed

+475
-89
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.core.convert;
17+
18+
import java.util.Collections;
19+
import java.util.Set;
20+
21+
import org.springframework.core.ResolvableType;
22+
import org.springframework.core.convert.ConversionService;
23+
import org.springframework.core.convert.TypeDescriptor;
24+
import org.springframework.core.convert.converter.GenericConverter;
25+
import org.springframework.data.convert.ReadingConverter;
26+
import org.springframework.data.convert.WritingConverter;
27+
import org.springframework.data.jdbc.core.mapping.AggregateReference;
28+
import org.springframework.lang.Nullable;
29+
import org.springframework.util.Assert;
30+
31+
/**
32+
* Converters for aggregate references. They need a {@link ConversionService} in order to delegate the conversion of the
33+
* content of the {@link AggregateReference}.
34+
*
35+
* @author Jens Schauder
36+
* @since 2.6
37+
*/
38+
final class AggregateReferenceConverters {
39+
40+
/**
41+
* Prevent instantiation.
42+
*/
43+
private AggregateReferenceConverters() {}
44+
45+
/**
46+
* Converts from an AggregateReference to its id, leaving the conversion of the id to the ultimate target type to the
47+
* delegate {@link ConversionService}.
48+
*/
49+
@WritingConverter
50+
public static class AggregateReferenceToSimpleTypeConverter implements GenericConverter {
51+
52+
private final ConversionService delegate;
53+
54+
public AggregateReferenceToSimpleTypeConverter(ConversionService delegate) {
55+
this.delegate = delegate;
56+
}
57+
58+
@Override
59+
public Set<ConvertiblePair> getConvertibleTypes() {
60+
return Collections.singleton(new ConvertiblePair(AggregateReference.class, Object.class));
61+
}
62+
63+
@Override
64+
public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) {
65+
66+
if (source == null) {
67+
return null;
68+
}
69+
70+
// if the target type is an AggregateReference we are going to assume it is of the correct type,
71+
// because it was already converted.
72+
Class<?> objectType = targetDescriptor.getObjectType();
73+
if (objectType.isAssignableFrom(AggregateReference.class)) {
74+
return source;
75+
}
76+
77+
Object id = ((AggregateReference<?, ?>) source).getId();
78+
79+
if (id == null) {
80+
throw new IllegalStateException(
81+
String.format("Aggregate references id must not be null when converting to %s from %s to %s", source,
82+
sourceDescriptor, targetDescriptor));
83+
}
84+
85+
return delegate.convert(id, TypeDescriptor.valueOf(id.getClass()), targetDescriptor);
86+
}
87+
}
88+
89+
/**
90+
* Convert any simple type to an {@link AggregateReference}. If the {@literal targetDescriptor} contains information
91+
* about the generic type id will properly get converted to the desired type by the delegate
92+
* {@link ConversionService}.
93+
*/
94+
@ReadingConverter
95+
public static class SimpleTypeToAggregateReferenceConverter implements GenericConverter {
96+
97+
private final ConversionService delegate;
98+
99+
public SimpleTypeToAggregateReferenceConverter(ConversionService delegate) {
100+
this.delegate = delegate;
101+
}
102+
103+
@Override
104+
public Set<ConvertiblePair> getConvertibleTypes() {
105+
return Collections.singleton(new ConvertiblePair(Object.class, AggregateReference.class));
106+
}
107+
108+
@Override
109+
public Object convert(@Nullable Object source, TypeDescriptor sourceDescriptor, TypeDescriptor targetDescriptor) {
110+
111+
if (source == null) {
112+
return null;
113+
}
114+
115+
ResolvableType componentType = targetDescriptor.getResolvableType().getGenerics()[1];
116+
TypeDescriptor targetType = TypeDescriptor.valueOf(componentType.resolve());
117+
Object convertedId = delegate.convert(source, TypeDescriptor.valueOf(source.getClass()), targetType);
118+
119+
return AggregateReference.to(convertedId);
120+
}
121+
}
122+
}

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

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import org.slf4j.LoggerFactory;
2727
import org.springframework.context.ApplicationContext;
2828
import org.springframework.context.ApplicationContextAware;
29+
import org.springframework.core.ResolvableType;
2930
import org.springframework.core.convert.ConverterNotFoundException;
31+
import org.springframework.core.convert.TypeDescriptor;
3032
import org.springframework.core.convert.converter.Converter;
3133
import org.springframework.data.convert.CustomConversions;
3234
import org.springframework.data.jdbc.core.mapping.AggregateReference;
@@ -219,20 +221,8 @@ public Object readValue(@Nullable Object value, TypeInformation<?> type) {
219221
return value;
220222
}
221223

222-
if (getConversions().hasCustomReadTarget(value.getClass(), type.getType())) {
223-
return getConversionService().convert(value, type.getType());
224-
}
225-
226-
if (AggregateReference.class.isAssignableFrom(type.getType())) {
227-
228-
if (type.getType().isAssignableFrom(value.getClass())) {
229-
return value;
230-
}
231-
232-
return readAggregateReference(value, type);
233-
}
234-
235-
if (value instanceof Array) {
224+
if ( !getConversions().hasCustomReadTarget(value.getClass(), type.getType()) &&
225+
value instanceof Array) {
236226
try {
237227
return readValue(((Array) value).getArray(), type);
238228
} catch (SQLException | ConverterNotFoundException e) {
@@ -243,14 +233,6 @@ public Object readValue(@Nullable Object value, TypeInformation<?> type) {
243233
return super.readValue(value, type);
244234
}
245235

246-
@SuppressWarnings("ConstantConditions")
247-
private Object readAggregateReference(@Nullable Object value, TypeInformation<?> type) {
248-
249-
TypeInformation<?> idType = type.getSuperTypeInformation(AggregateReference.class).getTypeArguments().get(1);
250-
251-
return AggregateReference.to(readValue(value, idType));
252-
}
253-
254236
/*
255237
* (non-Javadoc)
256238
* @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) {
263245
return null;
264246
}
265247

266-
if (AggregateReference.class.isAssignableFrom(value.getClass())) {
267-
return writeValue(((AggregateReference) value).getId(), type);
268-
}
269-
270248
return super.writeValue(value, type);
271249
}
272250

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
*/
1616
package org.springframework.data.jdbc.core.convert;
1717

18+
import java.util.ArrayList;
1819
import java.util.Collection;
1920
import java.util.Collections;
2021
import java.util.List;
2122

23+
import org.springframework.core.convert.ConversionService;
2224
import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair;
25+
import org.springframework.core.convert.support.DefaultConversionService;
2326
import org.springframework.data.convert.CustomConversions;
2427
import org.springframework.data.jdbc.core.mapping.JdbcSimpleTypes;
2528

@@ -36,8 +39,20 @@
3639
*/
3740
public class JdbcCustomConversions extends CustomConversions {
3841

39-
private static final Collection<Object> STORE_CONVERTERS = Collections
40-
.unmodifiableCollection(Jsr310TimestampBasedConverters.getConvertersToRegister());
42+
private static final Collection<Object> STORE_CONVERTERS;
43+
44+
static {
45+
46+
List<Object> converters = new ArrayList<>(Jsr310TimestampBasedConverters.getConvertersToRegister());
47+
48+
ConversionService conversionService = DefaultConversionService.getSharedInstance();
49+
converters.add(new AggregateReferenceConverters.AggregateReferenceToSimpleTypeConverter(conversionService));
50+
converters.add(new AggregateReferenceConverters.SimpleTypeToAggregateReferenceConverter(conversionService));
51+
52+
STORE_CONVERTERS = Collections.unmodifiableCollection(converters);
53+
54+
}
55+
4156
private static final StoreConversions STORE_CONVERSIONS = StoreConversions.of(JdbcSimpleTypes.HOLDER,
4257
STORE_CONVERTERS);
4358

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

Lines changed: 0 additions & 5 deletions
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
/**

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

Lines changed: 92 additions & 7 deletions
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

0 commit comments

Comments
 (0)