diff --git a/pom.xml b/pom.xml index f0b2a42b23..a79c50d133 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index 4626db4364..55b6d38479 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index 266af71b96..69097d7caa 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index ef353497fa..3e7450e6ca 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -17,25 +17,24 @@ import static org.springframework.data.jdbc.repository.query.JdbcQueryExecution.*; +import java.lang.reflect.Array; import java.lang.reflect.Constructor; +import java.sql.JDBCType; import java.sql.SQLType; import java.util.ArrayList; import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; -import java.util.Map; import java.util.function.Function; import java.util.function.Supplier; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; -import org.springframework.data.jdbc.core.convert.JdbcColumnTypes; import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.mapping.JdbcValue; -import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.relational.core.mapping.RelationalMappingContext; 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.repository.query.Parameter; import org.springframework.data.repository.query.Parameters; @@ -52,7 +51,6 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; -import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; /** @@ -72,7 +70,7 @@ */ public class StringBasedJdbcQuery extends AbstractJdbcQuery { - private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or when on Java 8+ use the javac flag -parameters"; + private static final String PARAMETER_NEEDS_TO_BE_NAMED = "For queries with named parameters you need to provide names for method parameters; Use @Param for query method parameters, or use the javac flag -parameters"; private final JdbcConverter converter; private final RowMapperFactory rowMapperFactory; private final SpelEvaluator spelEvaluator; @@ -185,53 +183,103 @@ private JdbcQueryExecution createJdbcQueryExecution(RelationalParameterAccess private MapSqlParameterSource bindParameters(RelationalParameterAccessor accessor) { - MapSqlParameterSource parameters = new MapSqlParameterSource(); - Parameters bindableParameters = accessor.getBindableParameters(); + MapSqlParameterSource parameters = new MapSqlParameterSource( + new LinkedHashMap<>(bindableParameters.getNumberOfParameters(), 1.0f)); for (Parameter bindableParameter : bindableParameters) { - convertAndAddParameter(parameters, bindableParameter, accessor.getBindableValue(bindableParameter.getIndex())); + + Object value = accessor.getBindableValue(bindableParameter.getIndex()); + String parameterName = bindableParameter.getName() + .orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); + JdbcParameters.JdbcParameter parameter = getQueryMethod().getParameters() + .getParameter(bindableParameter.getIndex()); + + JdbcValue jdbcValue = writeValue(value, parameter.getTypeInformation(), parameter); + SQLType jdbcType = jdbcValue.getJdbcType(); + + if (jdbcType == null) { + parameters.addValue(parameterName, jdbcValue.getValue()); + } else { + parameters.addValue(parameterName, jdbcValue.getValue(), jdbcType.getVendorTypeNumber()); + } } return parameters; } - private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter p, Object value) { + private JdbcValue writeValue(@Nullable Object value, TypeInformation typeInformation, + JdbcParameters.JdbcParameter parameter) { + + if (value == null) { + return JdbcValue.of(value, parameter.getSqlType()); + } + + if (typeInformation.isCollectionLike() && value instanceof Collection collection) { + + TypeInformation actualType = typeInformation.getActualType(); + + // tuple-binding + if (actualType != null && actualType.getType().isArray()) { + + TypeInformation nestedElementType = actualType.getRequiredActualType(); + return writeCollection(collection, JDBCType.OTHER, + array -> writeArrayValue(parameter, array, nestedElementType)); + } + + // parameter expansion + return writeCollection(collection, parameter.getActualSqlType(), + it -> converter.writeJdbcValue(it, typeInformation.getRequiredActualType(), parameter.getActualSqlType())); + } + + SQLType sqlType = parameter.getSqlType(); + return converter.writeJdbcValue(value, typeInformation, sqlType); + } - String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); + private JdbcValue writeCollection(Collection value, SQLType defaultType, Function mapper) { - JdbcParameters.JdbcParameter parameter = getQueryMethod().getParameters().getParameter(p.getIndex()); - TypeInformation typeInformation = parameter.getTypeInformation(); + if (value.isEmpty()) { + return JdbcValue.of(value, defaultType); + } JdbcValue jdbcValue; - if (typeInformation.isCollectionLike() && value instanceof Collection) { + List mapped = new ArrayList<>(value.size()); + SQLType jdbcType = null; + + for (Object o : value) { - List mapped = new ArrayList<>(); - SQLType jdbcType = null; + Object mappedValue = mapper.apply(o); - TypeInformation actualType = typeInformation.getRequiredActualType(); - for (Object o : (Iterable) value) { - JdbcValue elementJdbcValue = converter.writeJdbcValue(o, actualType, parameter.getActualSqlType()); + if (mappedValue instanceof JdbcValue jv) { if (jdbcType == null) { - jdbcType = elementJdbcValue.getJdbcType(); + jdbcType = jv.getJdbcType(); } - - mapped.add(elementJdbcValue.getValue()); + mappedValue = jv.getValue(); } - jdbcValue = JdbcValue.of(mapped, jdbcType); - } else { - SQLType sqlType = parameter.getSqlType(); - jdbcValue = converter.writeJdbcValue(value, typeInformation, sqlType); + mapped.add(mappedValue); } - SQLType jdbcType = jdbcValue.getJdbcType(); - if (jdbcType == null) { + jdbcValue = JdbcValue.of(mapped, jdbcType == null ? defaultType : jdbcType); - parameters.addValue(parameterName, jdbcValue.getValue()); - } else { - parameters.addValue(parameterName, jdbcValue.getValue(), jdbcType.getVendorTypeNumber()); + return jdbcValue; + } + + private Object[] writeArrayValue(JdbcParameters.JdbcParameter parameter, Object array, + TypeInformation nestedElementType) { + + int length = Array.getLength(array); + Object[] mappedArray = new Object[length]; + + for (int i = 0; i < length; i++) { + + Object element = Array.get(array, i); + JdbcValue elementJdbcValue = converter.writeJdbcValue(element, nestedElementType, parameter.getActualSqlType()); + + mappedArray[i] = elementJdbcValue.getValue(); } + + return mappedArray; } RowMapper determineRowMapper(ResultProcessor resultProcessor, boolean hasDynamicProjection) { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 9e343750e8..8fcfd73f4e 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -117,9 +117,13 @@ public class JdbcRepositoryIntegrationTests { @Autowired WithDelimitedColumnRepository withDelimitedColumnRepository; private static DummyEntity createDummyEntity() { + return createDummyEntity("Entity Name"); + } + + private static DummyEntity createDummyEntity(String entityName) { DummyEntity entity = new DummyEntity(); - entity.setName("Entity Name"); + entity.setName(entityName); return entity; } @@ -1334,6 +1338,23 @@ void withDelimitedColumnTest() { assertThat(inDatabase.get().getIdentifier()).isEqualTo("UR-123"); } + @Test // GH-1323 + void queryWithTupleIn() { + + DummyEntity one = repository.save(createDummyEntity("one")); + DummyEntity two = repository.save(createDummyEntity( "two")); + DummyEntity three = repository.save(createDummyEntity( "three")); + + List tuples = List.of( + new Object[]{two.idProp, "two"}, // matches "two" + new Object[]{three.idProp, "two"} // matches nothing + ); + + List result = repository.findByListInTuple(tuples); + + assertThat(result).containsOnly(two); + } + private Root createRoot(String namePrefix) { return new Root(null, namePrefix, @@ -1461,6 +1482,9 @@ interface DummyEntityRepository extends CrudRepository, Query Optional findDtoByIdProp(Long idProp); Optional findAllArgsDtoByIdProp(Long idProp); + + @Query("SELECT * FROM DUMMY_ENTITY WHERE (ID_PROP, NAME) IN (:tuples)") + List findByListInTuple(List tuples); } interface RootRepository extends ListCrudRepository { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index 8cccbab38e..2b3933e99d 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -22,6 +22,7 @@ import java.sql.JDBCType; import java.sql.ResultSet; import java.util.ArrayList; +import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.Properties; @@ -325,6 +326,33 @@ void appliesConverterToIterable() { assertThat(sqlParameterSource.getValue("value")).isEqualTo("one"); } + @Test // GH-1323 + void queryByListOfTuples() { + + String[][] tuples = {new String[]{"Albert", "Einstein"}, new String[]{"Richard", "Feynman"}}; + + SqlParameterSource parameterSource = forMethod("findByListOfTuples", List.class) // + .withArguments(Arrays.asList(tuples)) + .extractParameterSource(); + + assertThat(parameterSource.getValue("tuples")) + .asInstanceOf(LIST) + .containsExactly(tuples); + } + + @Test // GH-1323 + void queryByListOfConvertableTuples() { + + SqlParameterSource parameterSource = forMethod("findByListOfTuples", List.class) // + .withCustomConverters(DirectionToIntegerConverter.INSTANCE) // + .withArguments(Arrays.asList(new Object[]{Direction.LEFT, "Einstein"}, new Object[]{Direction.RIGHT, "Feynman"})) + .extractParameterSource(); + + assertThat(parameterSource.getValue("tuples")) + .asInstanceOf(LIST) + .containsExactly(new Object[][]{new Object[]{-1, "Einstein"}, new Object[]{1, "Feynman"}}); + } + QueryFixture forMethod(String name, Class... paramTypes) { return new QueryFixture(createMethod(name, paramTypes)); } @@ -450,6 +478,9 @@ interface MyRepository extends Repository { @Query("SELECT * FROM person WHERE lastname = $1") Object unsupportedLimitQuery(@Param("lastname") String lastname, Limit limit); + + @Query("select count(1) from person where (firstname, lastname) in (:tuples)") + Object findByListOfTuples(@Param("tuples") List tuples); } @Test // GH-619 diff --git a/spring-data-r2dbc/pom.xml b/spring-data-r2dbc/pom.xml index 3168f9d4f6..26830d77be 100644 --- a/spring-data-r2dbc/pom.xml +++ b/spring-data-r2dbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-r2dbc - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT Spring Data R2DBC Spring Data module for R2DBC @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 0287ece743..95ade84bba 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 3.4.0-SNAPSHOT + 3.4.0-1323-in-with-tuple-SNAPSHOT