Skip to content

Commit f97434d

Browse files
ctailor2schauder
authored andcommitted
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. Original pull request #1226 Closes #1212
1 parent 95f127f commit f97434d

21 files changed

+346
-34
lines changed

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

Lines changed: 33 additions & 5 deletions
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.JDBCType;
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.convert.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 {
@@ -157,11 +162,34 @@ private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter
157162

158163
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
159164

160-
Class<?> parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType();
161-
Class<?> conversionTargetType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(parameterType);
165+
RelationalParameters.RelationalParameter parameter = queryMethod.getParameters().getParameter(p.getIndex());
166+
ResolvableType resolvableType = parameter.getResolvableType();
167+
Class<?> type = resolvableType.resolve();
168+
Assert.notNull(type, "@Query parameter could not be resolved!");
162169

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

166194
JDBCType jdbcType = jdbcValue.getJdbcType();
167195
if (jdbcType == null) {

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

Lines changed: 113 additions & 7 deletions
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.
@@ -16,13 +16,15 @@
1616
package org.springframework.data.jdbc.repository;
1717

1818
import static java.util.Arrays.*;
19+
import static java.util.Collections.*;
1920
import static org.assertj.core.api.Assertions.*;
2021
import static org.assertj.core.api.SoftAssertions.*;
2122
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
2223

2324
import java.math.BigDecimal;
2425
import java.sql.JDBCType;
2526
import java.util.Date;
27+
import java.util.List;
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.convert.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,50 @@ 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(asList(Direction.LEFT, Direction.RIGHT)))
163+
.extracting(entity -> entity.direction).containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
164+
}
165+
166+
@Test // GH-1212
167+
void queryByEnumTypeEqual() {
168+
169+
EntityWithStringyBigDecimal entityA = new EntityWithStringyBigDecimal();
170+
entityA.direction = Direction.LEFT;
171+
EntityWithStringyBigDecimal entityB = new EntityWithStringyBigDecimal();
172+
entityB.direction = Direction.CENTER;
173+
EntityWithStringyBigDecimal entityC = new EntityWithStringyBigDecimal();
174+
entityC.direction = Direction.RIGHT;
175+
repository.saveAll(asList(entityA, entityB, entityC));
176+
177+
assertThat(repository.findByEnumTypeIn(singletonList(Direction.CENTER))).extracting(entity -> entity.direction)
178+
.containsExactly(Direction.CENTER);
179+
}
180+
181+
interface EntityWithStringyBigDecimalRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {
182+
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION IN (:types)")
183+
List<EntityWithStringyBigDecimal> findByEnumTypeIn(List<Direction> types);
184+
185+
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION = :type")
186+
List<EntityWithStringyBigDecimal> findByEnumType(Direction type);
187+
}
147188

148189
private static class EntityWithStringyBigDecimal {
149190

150191
@Id CustomId id;
151-
String stringyNumber;
192+
String stringyNumber = "1.0";
152193
OtherEntity reference;
194+
Direction direction = Direction.CENTER;
153195
}
154196

155197
private static class CustomId {
@@ -167,6 +209,10 @@ private static class OtherEntity {
167209
Date created;
168210
}
169211

212+
enum Direction {
213+
LEFT, CENTER, RIGHT
214+
}
215+
170216
@WritingConverter
171217
enum StringToBigDecimalConverter implements Converter<String, JdbcValue> {
172218

@@ -214,4 +260,64 @@ public CustomId convert(Number source) {
214260
}
215261
}
216262

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

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

Lines changed: 49 additions & 4 deletions
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;
@@ -73,11 +70,16 @@
7370
import org.springframework.test.jdbc.JdbcTestUtils;
7471
import org.springframework.transaction.annotation.Transactional;
7572

73+
import lombok.Data;
74+
import lombok.NoArgsConstructor;
75+
import lombok.Value;
76+
7677
/**
7778
* Very simple use cases for creation and usage of JdbcRepositories.
7879
*
7980
* @author Jens Schauder
8081
* @author Mark Paluch
82+
* @author Chirag Tailor
8183
*/
8284
@Transactional
8385
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
@@ -565,6 +567,38 @@ void nullStringResult() {
565567
assertThat(repository.returnInput(null)).isNull();
566568
}
567569

570+
@Test // GH-1212
571+
void queryByEnumTypeIn() {
572+
573+
DummyEntity dummyA = new DummyEntity("dummyA");
574+
dummyA.setDirection(Direction.LEFT);
575+
DummyEntity dummyB = new DummyEntity("dummyB");
576+
dummyB.setDirection(Direction.CENTER);
577+
DummyEntity dummyC = new DummyEntity("dummyC");
578+
dummyC.setDirection(Direction.RIGHT);
579+
repository.saveAll(asList(dummyA, dummyB, dummyC));
580+
581+
assertThat(repository.findByEnumTypeIn(asList(Direction.LEFT, Direction.RIGHT)))
582+
.extracting(DummyEntity::getDirection)
583+
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
584+
}
585+
586+
@Test // GH-1212
587+
void queryByEnumTypeEqual() {
588+
589+
DummyEntity dummyA = new DummyEntity("dummyA");
590+
dummyA.setDirection(Direction.LEFT);
591+
DummyEntity dummyB = new DummyEntity("dummyB");
592+
dummyB.setDirection(Direction.CENTER);
593+
DummyEntity dummyC = new DummyEntity("dummyC");
594+
dummyC.setDirection(Direction.RIGHT);
595+
repository.saveAll(asList(dummyA, dummyB, dummyC));
596+
597+
assertThat(repository.findByEnumType(Direction.CENTER))
598+
.extracting(DummyEntity::getDirection)
599+
.containsExactlyInAnyOrder(Direction.CENTER);
600+
}
601+
568602
private Instant createDummyBeforeAndAfterNow() {
569603

570604
Instant now = Instant.now();
@@ -645,6 +679,12 @@ interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
645679
@Query("SELECT CAST(:hello AS CHAR(5)) FROM DUMMY_ENTITY")
646680
@Nullable
647681
String returnInput(@Nullable String hello);
682+
683+
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION IN (:directions)")
684+
List<DummyEntity> findByEnumTypeIn(List<Direction> directions);
685+
686+
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION = :direction")
687+
List<DummyEntity> findByEnumType(Direction direction);
648688
}
649689

650690
@Configuration
@@ -698,12 +738,17 @@ static class DummyEntity {
698738
@Id private Long idProp;
699739
boolean flag;
700740
AggregateReference<DummyEntity, Long> ref;
741+
Direction direction;
701742

702743
public DummyEntity(String name) {
703744
this.name = name;
704745
}
705746
}
706747

748+
enum Direction {
749+
LEFT, CENTER, RIGHT
750+
}
751+
707752
interface DummyProjection {
708753

709754
String getName();

0 commit comments

Comments
 (0)