diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java index 77776c0ce..0ea262ef9 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryLookupStrategy.java @@ -67,7 +67,7 @@ class JdbcQueryLookupStrategy implements QueryLookupStrategy { RowMapper mapper = queryMethod.isModifyingQuery() ? null : createMapper(queryMethod); - return new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, mapper); + return new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, mapper, converter); } private RowMapper createMapper(JdbcQueryMethod queryMethod) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java index bfef9b4c9..800da87dc 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java @@ -16,17 +16,23 @@ package org.springframework.data.jdbc.repository.support; import java.lang.reflect.Constructor; +import java.sql.JDBCType; import java.util.List; import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcValue; +import org.springframework.data.jdbc.support.JdbcUtil; import org.springframework.data.mapping.callback.EntityCallbacks; +import org.springframework.data.relational.core.mapping.JdbcCompatibleTypes; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.RelationalPersistentEntity; import org.springframework.data.relational.core.mapping.event.AfterLoadCallback; import org.springframework.data.relational.core.mapping.event.AfterLoadEvent; import org.springframework.data.relational.core.mapping.event.Identifier; +import org.springframework.data.repository.query.Parameter; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.ResultSetExtractor; import org.springframework.jdbc.core.RowMapper; @@ -56,6 +62,7 @@ class JdbcRepositoryQuery implements RepositoryQuery { private final JdbcQueryMethod queryMethod; private final NamedParameterJdbcOperations operations; private final QueryExecutor executor; + private final JdbcConverter converter; /** * Creates a new {@link JdbcRepositoryQuery} for the given {@link JdbcQueryMethod}, {@link RelationalMappingContext} @@ -69,7 +76,7 @@ class JdbcRepositoryQuery implements RepositoryQuery { */ JdbcRepositoryQuery(ApplicationEventPublisher publisher, @Nullable EntityCallbacks callbacks, RelationalMappingContext context, JdbcQueryMethod queryMethod, NamedParameterJdbcOperations operations, - RowMapper defaultRowMapper) { + RowMapper defaultRowMapper, JdbcConverter converter) { Assert.notNull(publisher, "Publisher must not be null!"); Assert.notNull(context, "Context must not be null!"); @@ -93,6 +100,7 @@ class JdbcRepositoryQuery implements RepositoryQuery { rowMapper // ); + this.converter = converter; } private QueryExecutor createExecutor(JdbcQueryMethod queryMethod, @Nullable ResultSetExtractor extractor, @@ -209,13 +217,31 @@ class JdbcRepositoryQuery implements RepositoryQuery { queryMethod.getParameters().getBindableParameters().forEach(p -> { - String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); - parameters.addValue(parameterName, objects[p.getIndex()]); + convertAndAddParameter(parameters, p, objects[p.getIndex()]); }); return parameters; } + private void convertAndAddParameter(MapSqlParameterSource parameters, Parameter p, Object value) { + + String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); + + Class parameterType = queryMethod.getParameters().getParameter(p.getIndex()).getType(); + Class conversionTargetType = JdbcCompatibleTypes.INSTANCE.columnTypeForNonEntity(parameterType); + + JdbcValue jdbcValue = converter.writeJdbcValue(value, conversionTargetType, + JdbcUtil.sqlTypeFor(conversionTargetType)); + + JDBCType jdbcType = jdbcValue.getJdbcType(); + if (jdbcType == null) { + + parameters.addValue(parameterName, jdbcValue.getValue()); + }else { + parameters.addValue(parameterName, jdbcValue.getValue(), jdbcType.getVendorTypeNumber()); + } + } + @Nullable private ResultSetExtractor determineResultSetExtractor(@Nullable RowMapper rowMapper) { 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 4369b0af3..2195d0ca1 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 @@ -23,6 +23,9 @@ import lombok.Data; import java.io.IOException; import java.util.List; +import java.time.Instant; +import java.util.List; + import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; @@ -33,11 +36,13 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.core.io.ClassPathResource; import org.springframework.data.annotation.Id; +import org.springframework.data.jdbc.repository.query.Query; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.testing.TestConfiguration; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries; +import org.springframework.data.repository.query.Param; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.test.context.ContextConfiguration; @@ -258,6 +263,24 @@ public class JdbcRepositoryIntegrationTests { assertThat(repository.findById(-1L)).isEmpty(); } + @Test // DATAJDBC-464 + public void executeQueryWithParameterRequiringConversion() { + + Instant now = Instant.now(); + + DummyEntity first = repository.save(createDummyEntity()); + first.setPointInTime(now.minusSeconds(1000L)); + first.setName("first"); + + DummyEntity second = repository.save(createDummyEntity()); + second.setPointInTime(now.plusSeconds(1000L)); + second.setName("second"); + + repository.saveAll(asList(first, second)); + + assertThat(repository.after(now)).containsExactly(second); + } + @Test // DATAJDBC-234 public void findAllByQueryName() { @@ -269,17 +292,23 @@ public class JdbcRepositoryIntegrationTests { DummyEntity entity = new DummyEntity(); entity.setName("Entity Name"); + return entity; } interface DummyEntityRepository extends CrudRepository { List findAllByNamedQuery(); + + @Query("SELECT * FROM DUMMY_ENTITY WHERE POINT_IN_TIME > :threshhold") + List after(@Param("threshhold")Instant threshhold); + } @Data static class DummyEntity { String name; @Id private Long idProp; + Instant pointInTime; } } diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java index 40a7cd39f..66156dfcf 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java @@ -28,6 +28,9 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataAccessException; +import org.springframework.data.jdbc.core.convert.BasicJdbcConverter; +import org.springframework.data.jdbc.core.convert.JdbcConverter; +import org.springframework.data.jdbc.core.convert.RelationResolver; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.relational.core.mapping.RelationalMappingContext; import org.springframework.data.relational.core.mapping.event.AfterLoadCallback; @@ -58,6 +61,7 @@ public class JdbcRepositoryQueryUnitTests { ApplicationEventPublisher publisher; EntityCallbacks callbacks; RelationalMappingContext context; + JdbcConverter converter; @Before public void setup() throws NoSuchMethodException { @@ -73,6 +77,7 @@ public class JdbcRepositoryQueryUnitTests { this.publisher = mock(ApplicationEventPublisher.class); this.callbacks = mock(EntityCallbacks.class); this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS); + this.converter = new BasicJdbcConverter(context, mock(RelationResolver.class)); } @Test // DATAJDBC-165 @@ -82,7 +87,7 @@ public class JdbcRepositoryQueryUnitTests { Assertions.assertThatExceptionOfType(IllegalStateException.class) // .isThrownBy( - () -> new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + () -> new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {})); } @@ -92,7 +97,7 @@ public class JdbcRepositoryQueryUnitTests { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); doReturn(RowMapper.class).when(queryMethod).getRowMapperClass(); JdbcRepositoryQuery query = new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, - defaultRowMapper); + defaultRowMapper, converter); query.execute(new Object[] {}); @@ -104,7 +109,7 @@ public class JdbcRepositoryQueryUnitTests { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); JdbcRepositoryQuery query = new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, - defaultRowMapper); + defaultRowMapper, converter); query.execute(new Object[] {}); @@ -117,7 +122,7 @@ public class JdbcRepositoryQueryUnitTests { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass(); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {}); verify(operations) // @@ -130,7 +135,7 @@ public class JdbcRepositoryQueryUnitTests { doReturn("some sql statement").when(queryMethod).getDeclaredQuery(); doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass(); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {}); ArgumentCaptor captor = ArgumentCaptor.forClass(CustomResultSetExtractor.class); @@ -149,7 +154,7 @@ public class JdbcRepositoryQueryUnitTests { doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass(); doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass(); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {}); ArgumentCaptor captor = ArgumentCaptor.forClass(CustomResultSetExtractor.class); @@ -172,7 +177,7 @@ public class JdbcRepositoryQueryUnitTests { when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier()) .thenReturn("some identifier"); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {}); verify(publisher).publishEvent(any(AfterLoadEvent.class)); @@ -189,7 +194,7 @@ public class JdbcRepositoryQueryUnitTests { when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier()) .thenReturn("some identifier"); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper) + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter) .execute(new Object[] {}); verify(publisher, times(2)).publishEvent(any(AfterLoadEvent.class)); @@ -207,7 +212,7 @@ public class JdbcRepositoryQueryUnitTests { when(context.getRequiredPersistentEntity(DummyEntity.class).getIdentifierAccessor(any()).getIdentifier()) .thenReturn("some identifier"); - new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper).execute(new Object[] {}); + new JdbcRepositoryQuery(publisher, callbacks, context, queryMethod, operations, defaultRowMapper, converter).execute(new Object[] {}); verify(publisher).publishEvent(any(AfterLoadEvent.class)); verify(callbacks).callback(AfterLoadCallback.class, dummyEntity); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql index 7d747dff4..6649c1439 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-hsql.sql @@ -1 +1,6 @@ -CREATE TABLE dummy_entity ( id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, NAME VARCHAR(100)) +CREATE TABLE dummy_entity +( + id_Prop BIGINT GENERATED BY DEFAULT AS IDENTITY ( START WITH 1 ) PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql index 2f008796f..83aea089d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mariadb.sql @@ -1 +1,6 @@ -CREATE TABLE dummy_entity (id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE dummy_entity +( + id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP(3) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql index 937a66f1c..7569013f8 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mssql.sql @@ -1,2 +1,7 @@ DROP TABLE IF EXISTS dummy_entity; -CREATE TABLE dummy_entity (id_Prop BIGINT IDENTITY PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE dummy_entity +( + id_Prop BIGINT IDENTITY PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql index 2f008796f..83aea089d 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-mysql.sql @@ -1 +1,6 @@ -CREATE TABLE dummy_entity (id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE dummy_entity +( + id_Prop BIGINT AUTO_INCREMENT PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP(3) +); diff --git a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql index 7d6c03bb1..803ef2475 100644 --- a/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql +++ b/spring-data-jdbc/src/test/resources/org.springframework.data.jdbc.repository/JdbcRepositoryIntegrationTests-postgres.sql @@ -1,2 +1,7 @@ DROP TABLE dummy_entity; -CREATE TABLE dummy_entity (id_Prop SERIAL PRIMARY KEY, NAME VARCHAR(100)); +CREATE TABLE dummy_entity +( + id_Prop SERIAL PRIMARY KEY, + NAME VARCHAR(100), + POINT_IN_TIME TIMESTAMP +); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java index fe6f35149..a160c4b7c 100644 --- a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/BasicRelationalPersistentProperty.java @@ -49,15 +49,6 @@ import org.springframework.util.StringUtils; public class BasicRelationalPersistentProperty extends AnnotationBasedPersistentProperty implements RelationalPersistentProperty { - private static final Map, Class> javaToDbType = new LinkedHashMap<>(); - - static { - - javaToDbType.put(Enum.class, String.class); - javaToDbType.put(ZonedDateTime.class, String.class); - javaToDbType.put(Temporal.class, Date.class); - } - private final RelationalMappingContext context; private final Lazy columnName; private final Lazy> collectionIdColumnName; @@ -179,7 +170,7 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent return columnType; } - Class componentColumnType = columnTypeForNonEntity(getActualType()); + Class componentColumnType = JdbcCompatibleTypes.INSTANCE.columnTypeForNonEntity(getActualType()); while (componentColumnType.isArray()) { componentColumnType = componentColumnType.getComponentType(); @@ -286,15 +277,6 @@ public class BasicRelationalPersistentProperty extends AnnotationBasedPersistent return idProperty.getColumnType(); } - private Class columnTypeForNonEntity(Class type) { - - return javaToDbType.entrySet().stream() // - .filter(e -> e.getKey().isAssignableFrom(type)) // - .map(e -> (Class) e.getValue()) // - .findFirst() // - .orElseGet(() -> ClassUtils.resolvePrimitiveIfNecessary(type)); - } - private Class columnTypeForReference() { Class componentType = getTypeInformation().getRequiredComponentType().getType(); diff --git a/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/JdbcCompatibleTypes.java b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/JdbcCompatibleTypes.java new file mode 100644 index 000000000..1732386bf --- /dev/null +++ b/spring-data-relational/src/main/java/org/springframework/data/relational/core/mapping/JdbcCompatibleTypes.java @@ -0,0 +1,57 @@ +/* + * Copyright 2019 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.relational.core.mapping; + +import java.time.ZonedDateTime; +import java.time.temporal.Temporal; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.util.ClassUtils; + +/** + * Utility that determines the necessary type conversions between Java types used in the domain model and types + * compatible with JDBC drivers. + * + * @author Jens Schauder + * @since 2.0 + */ +public enum JdbcCompatibleTypes { + + INSTANCE { + + private final Map, Class> javaToDbType = new LinkedHashMap<>(); + + { + + javaToDbType.put(Enum.class, String.class); + javaToDbType.put(ZonedDateTime.class, String.class); + javaToDbType.put(Temporal.class, Date.class); + } + + public Class columnTypeForNonEntity(Class type) { + + return javaToDbType.entrySet().stream() // + .filter(e -> e.getKey().isAssignableFrom(type)) // + .map(e -> (Class) e.getValue()) // + .findFirst() // + .orElseGet(() -> ClassUtils.resolvePrimitiveIfNecessary(type)); + } + }; + + public abstract Class columnTypeForNonEntity(Class type); +}