Skip to content

Commit ffadf48

Browse files
committed
Update @query argument conversion to handle Collection<Enum>.
+ Copy logic from QueryMapper#convertToJdbcValue to resolve Iterable arguments on findBy* query methods to resolve the same for @query. + Use parameter ResolvableType instead of Class to retain generics info.
1 parent 2597454 commit ffadf48

22 files changed

+328
-37
lines changed

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

+1-2
Original file line numberDiff line numberDiff line change
@@ -267,8 +267,7 @@ public JdbcValue writeJdbcValue(@Nullable Object value, Class<?> columnType, int
267267
return writeJdbcValue(value, columnType, JdbcUtil.jdbcTypeFor(sqlType));
268268
}
269269

270-
271-
/*
270+
/*
272271
* (non-Javadoc)
273272
* @see org.springframework.data.jdbc.core.convert.JdbcConverter#writeValue(java.lang.Object, java.lang.Class, int)
274273
*/

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

+33-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2020-2021 the original author or authors.
2+
* Copyright 2020-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,16 +19,20 @@
1919

2020
import java.lang.reflect.Constructor;
2121
import java.sql.SQLType;
22+
import java.util.ArrayList;
23+
import java.util.List;
2224

2325
import org.springframework.beans.BeanUtils;
2426
import org.springframework.beans.factory.BeanFactory;
27+
import org.springframework.core.ResolvableType;
2528
import org.springframework.core.convert.converter.Converter;
2629
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
2730
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2831
import org.springframework.data.jdbc.core.mapping.JdbcValue;
2932
import org.springframework.data.jdbc.support.JdbcUtil;
3033
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
3134
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
35+
import org.springframework.data.relational.repository.query.RelationalParameters;
3236
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
3337
import org.springframework.data.repository.query.Parameter;
3438
import org.springframework.data.repository.query.Parameters;
@@ -53,6 +57,7 @@
5357
* @author Maciej Walkowiak
5458
* @author Mark Paluch
5559
* @author Hebert Coelho
60+
* @author Chirag Tailor
5661
* @since 2.0
5762
*/
5863
public class StringBasedJdbcQuery extends AbstractJdbcQuery {
@@ -149,11 +154,34 @@ private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter
149154

150155
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
151156

152-
Class<?> parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType();
153-
Class<?> conversionTargetType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(parameterType);
157+
RelationalParameters.RelationalParameter parameter = queryMethod.getParameters().getParameter(p.getIndex());
158+
ResolvableType resolvableType = parameter.getResolvableType();
159+
Class<?> type = resolvableType.resolve();
160+
Assert.notNull(type, "@Query parameter could not be resolved!");
154161

155-
JdbcValue jdbcValue = converter.writeJdbcValue(value, conversionTargetType,
156-
JdbcUtil.targetSqlTypeFor(conversionTargetType));
162+
JdbcValue jdbcValue;
163+
if (value instanceof Iterable) {
164+
165+
List<Object> mapped = new ArrayList<>();
166+
SQLType jdbcType = null;
167+
168+
Class<?> elementType = resolvableType.getGeneric(0).resolve();
169+
Assert.notNull(elementType, "@Query Iterable parameter generic type could not be resolved!");
170+
for (Object o : (Iterable<?>) value) {
171+
JdbcValue elementJdbcValue = converter.writeJdbcValue(o, elementType,
172+
JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(elementType)));
173+
if (jdbcType == null) {
174+
jdbcType = elementJdbcValue.getJdbcType();
175+
}
176+
177+
mapped.add(elementJdbcValue.getValue());
178+
}
179+
180+
jdbcValue = JdbcValue.of(mapped, jdbcType);
181+
} else {
182+
jdbcValue = converter.writeJdbcValue(value, type,
183+
JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(type)));
184+
}
157185

158186
SQLType jdbcType = jdbcValue.getJdbcType();
159187
if (jdbcType == null) {

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

+106-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2019-2021 the original author or authors.
2+
* Copyright 2019-2022 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -23,6 +23,8 @@
2323
import java.math.BigDecimal;
2424
import java.sql.JDBCType;
2525
import java.util.Date;
26+
import java.util.List;
27+
import java.util.Set;
2628

2729
import org.junit.jupiter.api.Test;
2830
import org.junit.jupiter.api.extension.ExtendWith;
@@ -36,6 +38,7 @@
3638
import org.springframework.data.convert.WritingConverter;
3739
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
3840
import org.springframework.data.jdbc.core.mapping.JdbcValue;
41+
import org.springframework.data.jdbc.repository.query.Query;
3942
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
4043
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
4144
import org.springframework.data.jdbc.testing.TestConfiguration;
@@ -50,6 +53,7 @@
5053
*
5154
* @author Jens Schauder
5255
* @author Sanghyuk Jung
56+
* @author Chirag Tailor
5357
*/
5458
@ContextConfiguration
5559
@Transactional
@@ -69,18 +73,19 @@ Class<?> testClass() {
6973
}
7074

7175
@Bean
72-
EntityWithBooleanRepository repository() {
73-
return factory.getRepository(EntityWithBooleanRepository.class);
76+
EntityWithStringyBigDecimalRepository repository() {
77+
return factory.getRepository(EntityWithStringyBigDecimalRepository.class);
7478
}
7579

7680
@Bean
7781
JdbcCustomConversions jdbcCustomConversions() {
7882
return new JdbcCustomConversions(asList(StringToBigDecimalConverter.INSTANCE, BigDecimalToString.INSTANCE,
79-
CustomIdReadingConverter.INSTANCE, CustomIdWritingConverter.INSTANCE));
83+
CustomIdReadingConverter.INSTANCE, CustomIdWritingConverter.INSTANCE, DirectionToIntegerConverter.INSTANCE,
84+
NumberToDirectionConverter.INSTANCE, IntegerToDirectionConverter.INSTANCE));
8085
}
8186
}
8287

83-
@Autowired EntityWithBooleanRepository repository;
88+
@Autowired EntityWithStringyBigDecimalRepository repository;
8489

8590
/**
8691
* In PostrgreSQL this fails if a simple converter like the following is used.
@@ -143,13 +148,52 @@ public void saveAndLoadAnEntityWithReference() {
143148
});
144149
}
145150

146-
interface EntityWithBooleanRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {}
151+
@Test // GH-1212
152+
void queryByEnumTypeIn() {
153+
154+
EntityWithStringyBigDecimal entityA = new EntityWithStringyBigDecimal();
155+
entityA.direction = Direction.LEFT;
156+
EntityWithStringyBigDecimal entityB = new EntityWithStringyBigDecimal();
157+
entityB.direction = Direction.CENTER;
158+
EntityWithStringyBigDecimal entityC = new EntityWithStringyBigDecimal();
159+
entityC.direction = Direction.RIGHT;
160+
repository.saveAll(asList(entityA, entityB, entityC));
161+
162+
assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT, Direction.RIGHT)))
163+
.extracting(entity -> entity.direction)
164+
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
165+
}
166+
167+
@Test // GH-1212
168+
void queryByEnumTypeEqual() {
169+
170+
EntityWithStringyBigDecimal entityA = new EntityWithStringyBigDecimal();
171+
entityA.direction = Direction.LEFT;
172+
EntityWithStringyBigDecimal entityB = new EntityWithStringyBigDecimal();
173+
entityB.direction = Direction.CENTER;
174+
EntityWithStringyBigDecimal entityC = new EntityWithStringyBigDecimal();
175+
entityC.direction = Direction.RIGHT;
176+
repository.saveAll(asList(entityA, entityB, entityC));
177+
178+
assertThat(repository.findByEnumTypeIn(Set.of(Direction.CENTER)))
179+
.extracting(entity -> entity.direction)
180+
.containsExactly(Direction.CENTER);
181+
}
182+
183+
interface EntityWithStringyBigDecimalRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {
184+
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION IN (:types)")
185+
List<EntityWithStringyBigDecimal> findByEnumTypeIn(Set<Direction> types);
186+
187+
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION = :type")
188+
List<EntityWithStringyBigDecimal> findByEnumType(Direction type);
189+
}
147190

148191
private static class EntityWithStringyBigDecimal {
149192

150193
@Id CustomId id;
151-
String stringyNumber;
194+
String stringyNumber = "1.0";
152195
OtherEntity reference;
196+
Direction direction = Direction.CENTER;
153197
}
154198

155199
private static class CustomId {
@@ -167,6 +211,10 @@ private static class OtherEntity {
167211
Date created;
168212
}
169213

214+
enum Direction {
215+
LEFT, CENTER, RIGHT
216+
}
217+
170218
@WritingConverter
171219
enum StringToBigDecimalConverter implements Converter<String, JdbcValue> {
172220

@@ -214,4 +262,55 @@ public CustomId convert(Number source) {
214262
}
215263
}
216264

265+
@WritingConverter
266+
enum DirectionToIntegerConverter implements Converter<Direction, JdbcValue> {
267+
268+
INSTANCE;
269+
270+
@Override
271+
public JdbcValue convert(Direction source) {
272+
273+
int integer = switch (source) {
274+
case LEFT -> -1;
275+
case CENTER -> 0;
276+
case RIGHT -> 1;
277+
};
278+
return JdbcValue.of(integer, JDBCType.INTEGER);
279+
}
280+
}
281+
282+
@ReadingConverter // Needed for Oracle since the JDBC driver returns BigDecimal on read
283+
enum NumberToDirectionConverter implements Converter<Number, Direction> {
284+
285+
INSTANCE;
286+
287+
@Override
288+
public Direction convert(Number source) {
289+
int sourceAsInt = source.intValue();
290+
if (sourceAsInt == 0) {
291+
return Direction.CENTER;
292+
} else if (sourceAsInt < 0) {
293+
return Direction.LEFT;
294+
} else {
295+
return Direction.RIGHT;
296+
}
297+
}
298+
}
299+
300+
@ReadingConverter
301+
enum IntegerToDirectionConverter implements Converter<Integer, Direction> {
302+
303+
INSTANCE;
304+
305+
@Override
306+
public Direction convert(Integer source) {
307+
if (source == 0) {
308+
return Direction.CENTER;
309+
} else if (source < 0) {
310+
return Direction.LEFT;
311+
} else {
312+
return Direction.RIGHT;
313+
}
314+
}
315+
}
217316
}

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

+50-5
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@
2020
import static org.assertj.core.api.SoftAssertions.*;
2121
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
2222

23-
import lombok.Data;
24-
import lombok.NoArgsConstructor;
25-
import lombok.Value;
26-
2723
import java.io.IOException;
2824
import java.sql.ResultSet;
2925
import java.time.Instant;
@@ -33,6 +29,7 @@
3329
import java.util.ArrayList;
3430
import java.util.Arrays;
3531
import java.util.List;
32+
import java.util.Set;
3633

3734
import org.junit.jupiter.api.BeforeEach;
3835
import org.junit.jupiter.api.Test;
@@ -50,7 +47,6 @@
5047
import org.springframework.data.domain.Pageable;
5148
import org.springframework.data.domain.Slice;
5249
import org.springframework.data.jdbc.core.mapping.AggregateReference;
53-
import org.springframework.data.relational.repository.Lock;
5450
import org.springframework.data.jdbc.repository.query.Modifying;
5551
import org.springframework.data.jdbc.repository.query.Query;
5652
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
@@ -62,6 +58,7 @@
6258
import org.springframework.data.relational.core.mapping.event.AfterConvertEvent;
6359
import org.springframework.data.relational.core.mapping.event.AfterLoadEvent;
6460
import org.springframework.data.relational.core.sql.LockMode;
61+
import org.springframework.data.relational.repository.Lock;
6562
import org.springframework.data.repository.CrudRepository;
6663
import org.springframework.data.repository.core.NamedQueries;
6764
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@@ -75,11 +72,16 @@
7572
import org.springframework.test.jdbc.JdbcTestUtils;
7673
import org.springframework.transaction.annotation.Transactional;
7774

75+
import lombok.Data;
76+
import lombok.NoArgsConstructor;
77+
import lombok.Value;
78+
7879
/**
7980
* Very simple use cases for creation and usage of JdbcRepositories.
8081
*
8182
* @author Jens Schauder
8283
* @author Mark Paluch
84+
* @author Chirag Tailor
8385
*/
8486
@Transactional
8587
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
@@ -575,6 +577,38 @@ void nullStringResult() {
575577
assertThat(repository.returnInput(null)).isNull();
576578
}
577579

580+
@Test // GH-1212
581+
void queryByEnumTypeIn() {
582+
583+
DummyEntity dummyA = new DummyEntity("dummyA");
584+
dummyA.setDirection(Direction.LEFT);
585+
DummyEntity dummyB = new DummyEntity("dummyB");
586+
dummyB.setDirection(Direction.CENTER);
587+
DummyEntity dummyC = new DummyEntity("dummyC");
588+
dummyC.setDirection(Direction.RIGHT);
589+
repository.saveAll(asList(dummyA, dummyB, dummyC));
590+
591+
assertThat(repository.findByEnumTypeIn(Set.of(Direction.LEFT, Direction.RIGHT)))
592+
.extracting(DummyEntity::getDirection)
593+
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
594+
}
595+
596+
@Test // GH-1212
597+
void queryByEnumTypeEqual() {
598+
599+
DummyEntity dummyA = new DummyEntity("dummyA");
600+
dummyA.setDirection(Direction.LEFT);
601+
DummyEntity dummyB = new DummyEntity("dummyB");
602+
dummyB.setDirection(Direction.CENTER);
603+
DummyEntity dummyC = new DummyEntity("dummyC");
604+
dummyC.setDirection(Direction.RIGHT);
605+
repository.saveAll(asList(dummyA, dummyB, dummyC));
606+
607+
assertThat(repository.findByEnumType(Direction.CENTER))
608+
.extracting(DummyEntity::getDirection)
609+
.containsExactlyInAnyOrder(Direction.CENTER);
610+
}
611+
578612
private Instant createDummyBeforeAndAfterNow() {
579613

580614
Instant now = Instant.now();
@@ -660,6 +694,12 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
660694
@Query("SELECT CAST(:hello AS CHAR(5)) FROM DUMMY_ENTITY")
661695
@Nullable
662696
String returnInput(@Nullable String hello);
697+
698+
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION IN (:directions)")
699+
List<DummyEntity> findByEnumTypeIn(Set<Direction> directions);
700+
701+
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION = :direction")
702+
List<DummyEntity> findByEnumType(Direction direction);
663703
}
664704

665705
@Configuration
@@ -713,12 +753,17 @@ static class DummyEntity {
713753
@Id private Long idProp;
714754
boolean flag;
715755
AggregateReference<DummyEntity, Long> ref;
756+
Direction direction;
716757

717758
public DummyEntity(String name) {
718759
this.name = name;
719760
}
720761
}
721762

763+
enum Direction {
764+
LEFT, CENTER, RIGHT
765+
}
766+
722767
interface DummyProjection {
723768

724769
String getName();

0 commit comments

Comments
 (0)