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 extends RowMapper> 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;
+ }
+ }
+}