diff --git a/pom.xml b/pom.xml index 634c3fe1f5..bb11328a1e 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-jdbc - 1.0.0.BUILD-SNAPSHOT + 1.0.0.DATAJDBC-165-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. diff --git a/src/main/java/org/springframework/data/jdbc/repository/query/Query.java b/src/main/java/org/springframework/data/jdbc/repository/query/Query.java index bf7037c8ff..c8c74294ad 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/query/Query.java +++ b/src/main/java/org/springframework/data/jdbc/repository/query/Query.java @@ -22,6 +22,7 @@ import java.lang.annotation.Target; import org.springframework.data.annotation.QueryAnnotation; +import org.springframework.jdbc.core.RowMapper; /** * Annotation to provide SQL statements that will get used for executing the method. @@ -36,5 +37,14 @@ @QueryAnnotation @Documented public @interface Query { + + /** + * The SQL statement to execute when the annotated method gets invoked. + */ String value(); + + /** + * Optional {@link RowMapper} to use to convert the result of the query to domain class instances. + */ + Class rowMapperClass() default RowMapper.class; } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java index ca748fe389..521514b7a3 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java @@ -28,9 +28,7 @@ /** * {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on - * that method. - * - * Binds method arguments to named parameters in the SQL statement. + * that method. Binds method arguments to named parameters in the SQL statement. * * @author Jens Schauder * @author Kazuki Shimizu @@ -40,6 +38,7 @@ public class JdbcQueryMethod extends QueryMethod { private final Method method; public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) { + super(method, metadata, factory); this.method = method; @@ -52,12 +51,18 @@ public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFac */ @Nullable public String getAnnotatedQuery() { + return getMergedAnnotationAttribute("value"); + } - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); - - return queryAnnotation == null ? null : queryAnnotation.value(); + /** + * Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper} + * + * @return May be {@code null}. + */ + public Class getRowMapperClass() { + return getMergedAnnotationAttribute("rowMapperClass"); } - + /** * Returns whether the query method is a modifying one. * @@ -68,4 +73,10 @@ public boolean isModifyingQuery() { return AnnotationUtils.findAnnotation(method, Modifying.class) != null; } + @SuppressWarnings("unchecked") + private T getMergedAnnotationAttribute(String attribute) { + + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + return (T) AnnotationUtils.getValue(queryAnnotation, attribute); + } } diff --git a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java index be48004c26..d927c4cae4 100644 --- a/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java +++ b/src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java @@ -15,11 +15,13 @@ */ package org.springframework.data.jdbc.repository.support; +import org.springframework.beans.BeanUtils; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.util.StringUtils; /** * A query to be executed based on a repository method, it's annotated SQL query and the arguments provided to the @@ -36,29 +38,34 @@ class JdbcRepositoryQuery implements RepositoryQuery { private final JdbcMappingContext context; private final RowMapper rowMapper; - JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper rowMapper) { + JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper defaultRowMapper) { this.queryMethod = queryMethod; this.context = context; - this.rowMapper = rowMapper; + this.rowMapper = createRowMapper(queryMethod, defaultRowMapper); + } + + private static RowMapper createRowMapper(JdbcQueryMethod queryMethod, RowMapper defaultRowMapper) { + + Class rowMapperClass = queryMethod.getRowMapperClass(); + + return rowMapperClass == null || rowMapperClass == RowMapper.class ? defaultRowMapper + : (RowMapper) BeanUtils.instantiateClass(rowMapperClass); } @Override public Object execute(Object[] objects) { - String query = queryMethod.getAnnotatedQuery(); - - MapSqlParameterSource parameters = new MapSqlParameterSource(); - queryMethod.getParameters().getBindableParameters().forEach(p -> { + String query = determineQuery(); - String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); - parameters.addValue(parameterName, objects[p.getIndex()]); - }); + MapSqlParameterSource parameters = bindParameters(objects); if (queryMethod.isModifyingQuery()) { + int updatedCount = context.getTemplate().update(query, parameters); Class returnedObjectType = queryMethod.getReturnedObjectType(); - return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0 : updatedCount; + return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0 + : updatedCount; } if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) { @@ -76,4 +83,25 @@ public Object execute(Object[] objects) { public JdbcQueryMethod getQueryMethod() { return queryMethod; } + + private String determineQuery() { + + String query = queryMethod.getAnnotatedQuery(); + + if (StringUtils.isEmpty(query)) { + throw new IllegalStateException(String.format("No query specified on %s", queryMethod.getName())); + } + return query; + } + + private MapSqlParameterSource bindParameters(Object[] objects) { + + MapSqlParameterSource parameters = new MapSqlParameterSource(); + queryMethod.getParameters().getBindableParameters().forEach(p -> { + + String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED)); + parameters.addValue(parameterName, objects[p.getIndex()]); + }); + return parameters; + } } diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java new file mode 100644 index 0000000000..c450740bfb --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethodUnitTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2018 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 + * + * http://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.jdbc.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; +import java.sql.ResultSet; + +import org.junit.Test; +import org.springframework.data.jdbc.repository.query.Query; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.jdbc.core.RowMapper; + +/** + * Unit tests for {@link JdbcQueryMethod}. + * + * @author Jens Schauder + */ +public class JdbcQueryMethodUnitTests { + + public static final String DUMMY_SELECT = "SELECT something"; + + @Test // DATAJDBC-165 + public void returnsSqlStatement() throws NoSuchMethodException { + + RepositoryMetadata metadata = mock(RepositoryMetadata.class); + when(metadata.getReturnedDomainClass(any(Method.class))).thenReturn((Class) String.class); + + JdbcQueryMethod queryMethod = new JdbcQueryMethod(JdbcQueryMethodUnitTests.class.getDeclaredMethod("queryMethod"), + metadata, mock(ProjectionFactory.class)); + + assertThat(queryMethod.getAnnotatedQuery()).isEqualTo(DUMMY_SELECT); + } + + @Test // DATAJDBC-165 + public void returnsSpecifiedRowMapperClass() throws NoSuchMethodException { + + RepositoryMetadata metadata = mock(RepositoryMetadata.class); + when(metadata.getReturnedDomainClass(any(Method.class))).thenReturn((Class) String.class); + + JdbcQueryMethod queryMethod = new JdbcQueryMethod(JdbcQueryMethodUnitTests.class.getDeclaredMethod("queryMethod"), + metadata, mock(ProjectionFactory.class)); + + assertThat(queryMethod.getRowMapperClass()).isEqualTo(CustomRowMapper.class); + } + + @Query(value = DUMMY_SELECT, rowMapperClass = CustomRowMapper.class) + private void queryMethod() {} + + private class CustomRowMapper implements RowMapper { + + @Override + public Object mapRow(ResultSet rs, int rowNum) { + return null; + } + } +} diff --git a/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java new file mode 100644 index 0000000000..2d93d43240 --- /dev/null +++ b/src/test/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQueryUnitTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 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 + * + * http://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.jdbc.repository.support; + +import org.assertj.core.api.Assertions; +import org.junit.Before; +import org.junit.Test; +import org.springframework.data.jdbc.mapping.model.JdbcMappingContext; +import org.springframework.data.repository.query.DefaultParameters; +import org.springframework.data.repository.query.Parameters; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +import java.sql.ResultSet; + +import static org.mockito.Mockito.*; + +/** + * Unit tests for {@link JdbcRepositoryQuery}. + * + * @author Jens Schauder + */ +public class JdbcRepositoryQueryUnitTests { + + JdbcQueryMethod queryMethod; + JdbcMappingContext context; + RowMapper defaultRowMapper; + JdbcRepositoryQuery query; + + @Before + public void setup() throws NoSuchMethodException { + + Parameters parameters = new DefaultParameters(JdbcRepositoryQueryUnitTests.class.getDeclaredMethod("dummyMethod")); + queryMethod = mock(JdbcQueryMethod.class); + when(queryMethod.getParameters()).thenReturn(parameters); + + context = mock(JdbcMappingContext.class, RETURNS_DEEP_STUBS); + defaultRowMapper = mock(RowMapper.class); + } + + @Test // DATAJDBC-165 + public void emptyQueryThrowsException() { + + when(queryMethod.getAnnotatedQuery()).thenReturn(null); + query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper); + + Assertions.assertThatExceptionOfType(IllegalStateException.class) // + .isThrownBy(() -> query.execute(new Object[]{})); + } + + @Test // DATAJDBC-165 + public void defaultRowMapperIsUsedByDefault() { + + when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement"); + when(queryMethod.getRowMapperClass()).thenReturn((Class) RowMapper.class); + query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper); + + query.execute(new Object[]{}); + + verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper)); + } + + @Test // DATAJDBC-165 + public void defaultRowMapperIsUsedForNull() { + + when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement"); + query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper); + + query.execute(new Object[]{}); + + verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper)); + } + + @Test // DATAJDBC-165 + public void customRowMapperIsUsedWhenSpecified() { + + when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement"); + when(queryMethod.getRowMapperClass()).thenReturn((Class) CustomRowMapper.class); + query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper); + + query.execute(new Object[]{}); + + verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), isA(CustomRowMapper.class)); + } + + /** + * The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup. + */ + private void dummyMethod() { + } + + private static class CustomRowMapper implements RowMapper { + @Override + public Object mapRow(ResultSet rs, int rowNum) { + return null; + } + } +}