Browse Source

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
2.3.x
Chirag Tailor 4 years ago committed by Jens Schauder
parent
commit
f97434de20
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 38
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java
  2. 120
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCustomConversionIntegrationTests.java
  3. 53
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  4. 116
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java
  5. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-db2.sql
  6. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql
  7. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql
  8. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql
  9. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql
  10. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql
  11. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-oracle.sql
  12. 2
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql
  13. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql
  14. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql
  15. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql
  16. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql
  17. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql
  18. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql
  19. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql
  20. 3
      spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql
  21. 12
      spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2020-2021 the original author or authors. * Copyright 2020-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,9 +19,12 @@ import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.
import java.lang.reflect.Constructor; import java.lang.reflect.Constructor;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcColumnTypes;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
@ -29,6 +32,7 @@ import org.springframework.data.jdbc.core.convert.JdbcValue;
import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.jdbc.support.JdbcUtil;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.relational.repository.query.RelationalParameters;
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor; import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
@ -53,6 +57,7 @@ import org.springframework.util.StringUtils;
* @author Maciej Walkowiak * @author Maciej Walkowiak
* @author Mark Paluch * @author Mark Paluch
* @author Hebert Coelho * @author Hebert Coelho
* @author Chirag Tailor
* @since 2.0 * @since 2.0
*/ */
public class StringBasedJdbcQuery extends AbstractJdbcQuery { public class StringBasedJdbcQuery extends AbstractJdbcQuery {
@ -157,11 +162,34 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
Class<?> parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType(); RelationalParameters.RelationalParameter parameter = queryMethod.getParameters().getParameter(p.getIndex());
Class<?> conversionTargetType = JdbcColumnTypes.INSTANCE.resolvePrimitiveType(parameterType); ResolvableType resolvableType = parameter.getResolvableType();
Class<?> type = resolvableType.resolve();
Assert.notNull(type, "@Query parameter could not be resolved!");
JdbcValue jdbcValue = converter.writeJdbcValue(value, conversionTargetType, JdbcValue jdbcValue;
JdbcUtil.sqlTypeFor(conversionTargetType)); if (value instanceof Iterable) {
List<Object> mapped = new ArrayList<>();
SQLType jdbcType = null;
Class<?> elementType = resolvableType.getGeneric(0).resolve();
Assert.notNull(elementType, "@Query Iterable parameter generic type could not be resolved!");
for (Object o : (Iterable<?>) value) {
JdbcValue elementJdbcValue = converter.writeJdbcValue(o, elementType,
JdbcUtil.targetSqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(elementType)));
if (jdbcType == null) {
jdbcType = elementJdbcValue.getJdbcType();
}
mapped.add(elementJdbcValue.getValue());
}
jdbcValue = JdbcValue.of(mapped, jdbcType);
} else {
jdbcValue = converter.writeJdbcValue(value, type,
JdbcUtil.sqlTypeFor(JdbcColumnTypes.INSTANCE.resolvePrimitiveType(type)));
}
JDBCType jdbcType = jdbcValue.getJdbcType(); JDBCType jdbcType = jdbcValue.getJdbcType();
if (jdbcType == null) { if (jdbcType == null) {

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

@ -1,5 +1,5 @@
/* /*
* Copyright 2019-2021 the original author or authors. * Copyright 2019-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -16,6 +16,7 @@
package org.springframework.data.jdbc.repository; package org.springframework.data.jdbc.repository;
import static java.util.Arrays.*; import static java.util.Arrays.*;
import static java.util.Collections.*;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.SoftAssertions.*; import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
@ -23,6 +24,7 @@ import static org.springframework.test.context.TestExecutionListeners.MergeMode.
import java.math.BigDecimal; import java.math.BigDecimal;
import java.sql.JDBCType; import java.sql.JDBCType;
import java.util.Date; import java.util.Date;
import java.util.List;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@ -36,6 +38,7 @@ import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter; import org.springframework.data.convert.WritingConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions; import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.convert.JdbcValue; import org.springframework.data.jdbc.core.convert.JdbcValue;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener; import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.jdbc.testing.TestConfiguration;
@ -50,6 +53,7 @@ import org.springframework.transaction.annotation.Transactional;
* *
* @author Jens Schauder * @author Jens Schauder
* @author Sanghyuk Jung * @author Sanghyuk Jung
* @author Chirag Tailor
*/ */
@ContextConfiguration @ContextConfiguration
@Transactional @Transactional
@ -69,18 +73,19 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
} }
@Bean @Bean
EntityWithBooleanRepository repository() { EntityWithStringyBigDecimalRepository repository() {
return factory.getRepository(EntityWithBooleanRepository.class); return factory.getRepository(EntityWithStringyBigDecimalRepository.class);
} }
@Bean @Bean
JdbcCustomConversions jdbcCustomConversions() { JdbcCustomConversions jdbcCustomConversions() {
return new JdbcCustomConversions(asList(StringToBigDecimalConverter.INSTANCE, BigDecimalToString.INSTANCE, return new JdbcCustomConversions(asList(StringToBigDecimalConverter.INSTANCE, BigDecimalToString.INSTANCE,
CustomIdReadingConverter.INSTANCE, CustomIdWritingConverter.INSTANCE)); CustomIdReadingConverter.INSTANCE, CustomIdWritingConverter.INSTANCE, DirectionToIntegerConverter.INSTANCE,
NumberToDirectionConverter.INSTANCE, IntegerToDirectionConverter.INSTANCE));
} }
} }
@Autowired EntityWithBooleanRepository repository; @Autowired EntityWithStringyBigDecimalRepository repository;
/** /**
* In PostrgreSQL this fails if a simple converter like the following is used. * In PostrgreSQL this fails if a simple converter like the following is used.
@ -143,13 +148,50 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
}); });
} }
interface EntityWithBooleanRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {} @Test // GH-1212
void queryByEnumTypeIn() {
EntityWithStringyBigDecimal entityA = new EntityWithStringyBigDecimal();
entityA.direction = Direction.LEFT;
EntityWithStringyBigDecimal entityB = new EntityWithStringyBigDecimal();
entityB.direction = Direction.CENTER;
EntityWithStringyBigDecimal entityC = new EntityWithStringyBigDecimal();
entityC.direction = Direction.RIGHT;
repository.saveAll(asList(entityA, entityB, entityC));
assertThat(repository.findByEnumTypeIn(asList(Direction.LEFT, Direction.RIGHT)))
.extracting(entity -> entity.direction).containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
}
@Test // GH-1212
void queryByEnumTypeEqual() {
EntityWithStringyBigDecimal entityA = new EntityWithStringyBigDecimal();
entityA.direction = Direction.LEFT;
EntityWithStringyBigDecimal entityB = new EntityWithStringyBigDecimal();
entityB.direction = Direction.CENTER;
EntityWithStringyBigDecimal entityC = new EntityWithStringyBigDecimal();
entityC.direction = Direction.RIGHT;
repository.saveAll(asList(entityA, entityB, entityC));
assertThat(repository.findByEnumTypeIn(singletonList(Direction.CENTER))).extracting(entity -> entity.direction)
.containsExactly(Direction.CENTER);
}
interface EntityWithStringyBigDecimalRepository extends CrudRepository<EntityWithStringyBigDecimal, CustomId> {
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION IN (:types)")
List<EntityWithStringyBigDecimal> findByEnumTypeIn(List<Direction> types);
@Query("SELECT * FROM ENTITY_WITH_STRINGY_BIG_DECIMAL WHERE DIRECTION = :type")
List<EntityWithStringyBigDecimal> findByEnumType(Direction type);
}
private static class EntityWithStringyBigDecimal { private static class EntityWithStringyBigDecimal {
@Id CustomId id; @Id CustomId id;
String stringyNumber; String stringyNumber = "1.0";
OtherEntity reference; OtherEntity reference;
Direction direction = Direction.CENTER;
} }
private static class CustomId { private static class CustomId {
@ -167,6 +209,10 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
Date created; Date created;
} }
enum Direction {
LEFT, CENTER, RIGHT
}
@WritingConverter @WritingConverter
enum StringToBigDecimalConverter implements Converter<String, JdbcValue> { enum StringToBigDecimalConverter implements Converter<String, JdbcValue> {
@ -214,4 +260,64 @@ public class JdbcRepositoryCustomConversionIntegrationTests {
} }
} }
@WritingConverter
enum DirectionToIntegerConverter implements Converter<Direction, JdbcValue> {
INSTANCE;
@Override
public JdbcValue convert(Direction source) {
int integer;
switch (source) {
case LEFT:
integer = -1;
break;
case CENTER:
integer = 0;
break;
case RIGHT:
integer = 1;
break;
default:
throw new IllegalArgumentException();
}
return JdbcValue.of(integer, JDBCType.INTEGER);
}
}
@ReadingConverter // Needed for Oracle since the JDBC driver returns BigDecimal on read
enum NumberToDirectionConverter implements Converter<Number, Direction> {
INSTANCE;
@Override
public Direction convert(Number source) {
int sourceAsInt = source.intValue();
if (sourceAsInt == 0) {
return Direction.CENTER;
} else if (sourceAsInt < 0) {
return Direction.LEFT;
} else {
return Direction.RIGHT;
}
}
}
@ReadingConverter
enum IntegerToDirectionConverter implements Converter<Integer, Direction> {
INSTANCE;
@Override
public Direction convert(Integer source) {
if (source == 0) {
return Direction.CENTER;
} else if (source < 0) {
return Direction.LEFT;
} else {
return Direction.RIGHT;
}
}
}
} }

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

@ -20,10 +20,6 @@ import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.SoftAssertions.*; import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;
import java.io.IOException; import java.io.IOException;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.time.Instant; import java.time.Instant;
@ -33,6 +29,7 @@ import java.time.ZoneOffset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -73,11 +70,16 @@ import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.test.jdbc.JdbcTestUtils;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Value;
/** /**
* Very simple use cases for creation and usage of JdbcRepositories. * Very simple use cases for creation and usage of JdbcRepositories.
* *
* @author Jens Schauder * @author Jens Schauder
* @author Mark Paluch * @author Mark Paluch
* @author Chirag Tailor
*/ */
@Transactional @Transactional
@TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS) @TestExecutionListeners(value = AssumeFeatureTestExecutionListener.class, mergeMode = MERGE_WITH_DEFAULTS)
@ -565,6 +567,38 @@ public class JdbcRepositoryIntegrationTests {
assertThat(repository.returnInput(null)).isNull(); assertThat(repository.returnInput(null)).isNull();
} }
@Test // GH-1212
void queryByEnumTypeIn() {
DummyEntity dummyA = new DummyEntity("dummyA");
dummyA.setDirection(Direction.LEFT);
DummyEntity dummyB = new DummyEntity("dummyB");
dummyB.setDirection(Direction.CENTER);
DummyEntity dummyC = new DummyEntity("dummyC");
dummyC.setDirection(Direction.RIGHT);
repository.saveAll(asList(dummyA, dummyB, dummyC));
assertThat(repository.findByEnumTypeIn(asList(Direction.LEFT, Direction.RIGHT)))
.extracting(DummyEntity::getDirection)
.containsExactlyInAnyOrder(Direction.LEFT, Direction.RIGHT);
}
@Test // GH-1212
void queryByEnumTypeEqual() {
DummyEntity dummyA = new DummyEntity("dummyA");
dummyA.setDirection(Direction.LEFT);
DummyEntity dummyB = new DummyEntity("dummyB");
dummyB.setDirection(Direction.CENTER);
DummyEntity dummyC = new DummyEntity("dummyC");
dummyC.setDirection(Direction.RIGHT);
repository.saveAll(asList(dummyA, dummyB, dummyC));
assertThat(repository.findByEnumType(Direction.CENTER))
.extracting(DummyEntity::getDirection)
.containsExactlyInAnyOrder(Direction.CENTER);
}
private Instant createDummyBeforeAndAfterNow() { private Instant createDummyBeforeAndAfterNow() {
Instant now = Instant.now(); Instant now = Instant.now();
@ -645,6 +679,12 @@ public class JdbcRepositoryIntegrationTests {
@Query("SELECT CAST(:hello AS CHAR(5)) FROM DUMMY_ENTITY") @Query("SELECT CAST(:hello AS CHAR(5)) FROM DUMMY_ENTITY")
@Nullable @Nullable
String returnInput(@Nullable String hello); String returnInput(@Nullable String hello);
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION IN (:directions)")
List<DummyEntity> findByEnumTypeIn(List<Direction> directions);
@Query("SELECT * FROM DUMMY_ENTITY WHERE DIRECTION = :direction")
List<DummyEntity> findByEnumType(Direction direction);
} }
@Configuration @Configuration
@ -698,12 +738,17 @@ public class JdbcRepositoryIntegrationTests {
@Id private Long idProp; @Id private Long idProp;
boolean flag; boolean flag;
AggregateReference<DummyEntity, Long> ref; AggregateReference<DummyEntity, Long> ref;
Direction direction;
public DummyEntity(String name) { public DummyEntity(String name) {
this.name = name; this.name = name;
} }
} }
enum Direction {
LEFT, CENTER, RIGHT
}
interface DummyProjection { interface DummyProjection {
String getName(); String getName();

116
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java

@ -1,5 +1,5 @@
/* /*
* Copyright 2020-2021 the original author or authors. * Copyright 2020-2022 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,28 +15,38 @@
*/ */
package org.springframework.data.jdbc.repository.query; package org.springframework.data.jdbc.repository.query;
import static java.util.Arrays.*;
import static org.assertj.core.api.Assertions.*; import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*; import static org.mockito.Mockito.*;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.sql.JDBCType;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.util.List; import java.util.List;
import java.util.Properties; import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.assertj.core.api.Assertions; import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessException;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcCustomConversions;
import org.springframework.data.jdbc.core.convert.JdbcTypeFactory;
import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.jdbc.core.mapping.JdbcValue;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.sql.IdentifierProcessing;
import org.springframework.data.repository.Repository; import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
@ -55,6 +65,7 @@ import org.springframework.util.ReflectionUtils;
* @author Evgeni Dimitrov * @author Evgeni Dimitrov
* @author Mark Paluch * @author Mark Paluch
* @author Dennis Effing * @author Dennis Effing
* @author Chirag Tailor
*/ */
class StringBasedJdbcQueryUnitTests { class StringBasedJdbcQueryUnitTests {
@ -64,7 +75,7 @@ class StringBasedJdbcQueryUnitTests {
JdbcConverter converter; JdbcConverter converter;
@BeforeEach @BeforeEach
void setup() throws NoSuchMethodException { void setup() {
this.defaultRowMapper = mock(RowMapper.class); this.defaultRowMapper = mock(RowMapper.class);
this.operations = mock(NamedParameterJdbcOperations.class); this.operations = mock(NamedParameterJdbcOperations.class);
@ -172,6 +183,54 @@ class StringBasedJdbcQueryUnitTests {
.hasMessageContaining("Page queries are not supported using string-based queries"); .hasMessageContaining("Page queries are not supported using string-based queries");
} }
@Test // GH-1212
void convertsEnumCollectionParameterIntoStringCollectionParameter() {
JdbcQueryMethod queryMethod = createMethod("findByEnumTypeIn", Set.class);
BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class));
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter);
query.execute(new Object[] { asList(Direction.LEFT, Direction.RIGHT) });
ArgumentCaptor<SqlParameterSource> captor = ArgumentCaptor.forClass(SqlParameterSource.class);
verify(operations).query(anyString(), captor.capture(), any(ResultSetExtractor.class));
SqlParameterSource sqlParameterSource = captor.getValue();
assertThat(sqlParameterSource.getValue("directions")).asList().containsExactlyInAnyOrder("LEFT", "RIGHT");
}
@Test // GH-1212
void convertsEnumCollectionParameterUsingCustomConverterWhenRegisteredForType() {
JdbcQueryMethod queryMethod = createMethod("findByEnumTypeIn", Set.class);
BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class), new JdbcCustomConversions(asList(DirectionToIntegerConverter.INSTANCE, IntegerToDirectionConverter.INSTANCE)), JdbcTypeFactory.unsupported(), IdentifierProcessing.ANSI);
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter);
query.execute(new Object[] { asList(Direction.LEFT, Direction.RIGHT) });
ArgumentCaptor<SqlParameterSource> captor = ArgumentCaptor.forClass(SqlParameterSource.class);
verify(operations).query(anyString(), captor.capture(), any(ResultSetExtractor.class));
SqlParameterSource sqlParameterSource = captor.getValue();
assertThat(sqlParameterSource.getValue("directions")).asList().containsExactlyInAnyOrder(-1, 1);
}
@Test // GH-1212
void doesNotConvertNonCollectionParameter() {
JdbcQueryMethod queryMethod = createMethod("findBySimpleValue", Integer.class);
BasicJdbcConverter converter = new BasicJdbcConverter(mock(RelationalMappingContext.class), mock(RelationResolver.class));
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, result -> mock(RowMapper.class), converter);
query.execute(new Object[] { 1 });
ArgumentCaptor<SqlParameterSource> captor = ArgumentCaptor.forClass(SqlParameterSource.class);
verify(operations).query(anyString(), captor.capture(), any(ResultSetExtractor.class));
SqlParameterSource sqlParameterSource = captor.getValue();
assertThat(sqlParameterSource.getValue("value")).isEqualTo(1);
}
private JdbcQueryMethod createMethod(String methodName, Class<?>... paramTypes) { private JdbcQueryMethod createMethod(String methodName, Class<?>... paramTypes) {
Method method = ReflectionUtils.findMethod(MyRepository.class, methodName, paramTypes); Method method = ReflectionUtils.findMethod(MyRepository.class, methodName, paramTypes);
@ -212,6 +271,11 @@ class StringBasedJdbcQueryUnitTests {
@Query(value = "some sql statement") @Query(value = "some sql statement")
Slice<Object> sliceAll(Pageable pageable); Slice<Object> sliceAll(Pageable pageable);
@Query(value = "some sql statement")
List<Object> findByEnumTypeIn(Set<Direction> directions);
@Query(value = "some sql statement")
List<Object> findBySimpleValue(Integer value);
} }
private static class CustomRowMapper implements RowMapper<Object> { private static class CustomRowMapper implements RowMapper<Object> {
@ -241,6 +305,54 @@ class StringBasedJdbcQueryUnitTests {
} }
} }
private enum Direction {
LEFT, CENTER, RIGHT
}
@WritingConverter
enum DirectionToIntegerConverter implements Converter<Direction, JdbcValue> {
INSTANCE;
@Override
public JdbcValue convert(Direction source) {
int integer;
switch (source) {
case LEFT:
integer = -1;
break;
case CENTER:
integer = 0;
break;
case RIGHT:
integer = 1;
break;
default:
throw new IllegalArgumentException();
}
return JdbcValue.of(integer, JDBCType.INTEGER);
}
}
@ReadingConverter
enum IntegerToDirectionConverter implements Converter<Integer, Direction> {
INSTANCE;
@Override
public Direction convert(Integer source) {
if (source == 0) {
return Direction.CENTER;
} else if (source < 0) {
return Direction.LEFT;
} else {
return Direction.RIGHT;
}
}
}
private static class DummyEntity { private static class DummyEntity {
private Long id; private Long id;

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-db2.sql

@ -1,5 +1,5 @@
DROP TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL; DROP TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL;
DROP TABLE OTHER_ENTITY; DROP TABLE OTHER_ENTITY;
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID BIGINT GENERATED BY DEFAULT AS IDENTITY(START WITH 1) PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-h2.sql

@ -1,2 +1,2 @@
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-hsql.sql

@ -1,3 +1,3 @@
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mariadb.sql

@ -1,2 +1,2 @@
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mssql.sql

@ -1,5 +1,5 @@
DROP TABLE OTHER_ENTITY; DROP TABLE OTHER_ENTITY;
DROP TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL; DROP TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL;
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id BIGINT IDENTITY PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID BIGINT IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID BIGINT IDENTITY PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-mysql.sql

@ -1,2 +1,2 @@
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID BIGINT AUTO_INCREMENT PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-oracle.sql

@ -3,7 +3,8 @@ DROP TABLE OTHER_ENTITY CASCADE CONSTRAINTS PURGE;
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL (
ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY, ID NUMBER GENERATED BY DEFAULT ON NULL AS IDENTITY PRIMARY KEY,
STRINGY_NUMBER DECIMAL(20,10) STRINGY_NUMBER DECIMAL(20,10),
DIRECTION INTEGER
); );
CREATE TABLE OTHER_ENTITY ( CREATE TABLE OTHER_ENTITY (

2
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryCustomConversionIntegrationTests-postgres.sql

@ -1,2 +1,2 @@
CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10)); CREATE TABLE ENTITY_WITH_STRINGY_BIG_DECIMAL ( id SERIAL PRIMARY KEY, Stringy_number DECIMAL(20,10), DIRECTION INTEGER);
CREATE TABLE OTHER_ENTITY ( ID SERIAL PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER); CREATE TABLE OTHER_ENTITY ( ID SERIAL PRIMARY KEY, CREATED DATE, ENTITY_WITH_STRINGY_BIG_DECIMAL INTEGER);

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-db2.sql

@ -7,5 +7,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP, -- with time zone is only supported with z/OS OFFSET_DATE_TIME TIMESTAMP, -- with time zone is only supported with z/OS
FLAG BOOLEAN, FLAG BOOLEAN,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-h2.sql

@ -5,5 +5,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN, FLAG BOOLEAN,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql

@ -5,5 +5,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN, FLAG BOOLEAN,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql

@ -5,5 +5,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME TIMESTAMP(3), POINT_IN_TIME TIMESTAMP(3),
OFFSET_DATE_TIME TIMESTAMP(3), OFFSET_DATE_TIME TIMESTAMP(3),
FLAG BOOLEAN, FLAG BOOLEAN,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql

@ -6,5 +6,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME DATETIME, POINT_IN_TIME DATETIME,
OFFSET_DATE_TIME DATETIMEOFFSET, OFFSET_DATE_TIME DATETIMEOFFSET,
FLAG BIT, FLAG BIT,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql

@ -8,5 +8,6 @@ CREATE TABLE DUMMY_ENTITY
POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL, POINT_IN_TIME TIMESTAMP(3) DEFAULT NULL,
OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL, OFFSET_DATE_TIME TIMESTAMP(3) DEFAULT NULL,
FLAG BIT(1), FLAG BIT(1),
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-oracle.sql

@ -7,5 +7,6 @@ CREATE TABLE DUMMY_ENTITY
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG NUMBER(1,0), FLAG NUMBER(1,0),
REF NUMBER REF NUMBER,
DIRECTION VARCHAR2(100)
); );

3
spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql

@ -6,5 +6,6 @@ CREATE TABLE dummy_entity
POINT_IN_TIME TIMESTAMP, POINT_IN_TIME TIMESTAMP,
OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE, OFFSET_DATE_TIME TIMESTAMP WITH TIME ZONE,
FLAG BOOLEAN, FLAG BOOLEAN,
REF BIGINT REF BIGINT,
DIRECTION VARCHAR(100)
); );

12
spring-data-relational/src/main/java/org/springframework/data/relational/repository/query/RelationalParameters.java

@ -16,9 +16,11 @@
package org.springframework.data.relational.repository.query; package org.springframework.data.relational.repository.query;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.List; import java.util.List;
import org.springframework.core.MethodParameter; import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter; import org.springframework.data.relational.repository.query.RelationalParameters.RelationalParameter;
import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.Parameter;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
@ -65,9 +67,12 @@ public class RelationalParameters extends Parameters<RelationalParameters, Relat
* Custom {@link Parameter} implementation. * Custom {@link Parameter} implementation.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Chirag Tailor
*/ */
public static class RelationalParameter extends Parameter { public static class RelationalParameter extends Parameter {
private final MethodParameter parameter;
/** /**
* Creates a new {@link RelationalParameter}. * Creates a new {@link RelationalParameter}.
* *
@ -75,6 +80,13 @@ public class RelationalParameters extends Parameters<RelationalParameters, Relat
*/ */
RelationalParameter(MethodParameter parameter) { RelationalParameter(MethodParameter parameter) {
super(parameter); super(parameter);
this.parameter = parameter;
}
public ResolvableType getResolvableType() {
return ResolvableType
.forClassWithGenerics(super.getType(), ResolvableType.forMethodParameter(this.parameter).getGenerics());
} }
} }
} }

Loading…
Cancel
Save