Skip to content

Commit 043bd4d

Browse files
schaudergregturn
authored andcommitted
DATAJDBC-165 - Allows configuration of custom RowMapper on @query annotation.
1 parent 5f6a44d commit 043bd4d

File tree

5 files changed

+250
-17
lines changed

5 files changed

+250
-17
lines changed

src/main/java/org/springframework/data/jdbc/repository/query/Query.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import java.lang.annotation.Target;
2323

2424
import org.springframework.data.annotation.QueryAnnotation;
25+
import org.springframework.jdbc.core.RowMapper;
2526

2627
/**
2728
* Annotation to provide SQL statements that will get used for executing the method.
@@ -36,5 +37,14 @@
3637
@QueryAnnotation
3738
@Documented
3839
public @interface Query {
40+
41+
/**
42+
* The SQL statement to execute when the annotated method gets invoked.
43+
*/
3944
String value();
45+
46+
/**
47+
* Optional {@link RowMapper} to use to convert the result of the query to domain class instances.
48+
*/
49+
Class<? extends RowMapper> rowMapperClass() default RowMapper.class;
4050
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcQueryMethod.java

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,7 @@
2828

2929
/**
3030
* {@link QueryMethod} implementation that implements a method by executing the query from a {@link Query} annotation on
31-
* that method.
32-
*
33-
* Binds method arguments to named parameters in the SQL statement.
31+
* that method. Binds method arguments to named parameters in the SQL statement.
3432
*
3533
* @author Jens Schauder
3634
* @author Kazuki Shimizu
@@ -40,6 +38,7 @@ public class JdbcQueryMethod extends QueryMethod {
4038
private final Method method;
4139

4240
public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFactory factory) {
41+
4342
super(method, metadata, factory);
4443

4544
this.method = method;
@@ -52,12 +51,18 @@ public JdbcQueryMethod(Method method, RepositoryMetadata metadata, ProjectionFac
5251
*/
5352
@Nullable
5453
public String getAnnotatedQuery() {
54+
return getMergedAnnotationAttribute("value");
55+
}
5556

56-
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
57-
58-
return queryAnnotation == null ? null : queryAnnotation.value();
57+
/**
58+
* Returns the class to be used as {@link org.springframework.jdbc.core.RowMapper}
59+
*
60+
* @return May be {@code null}.
61+
*/
62+
public Class<?> getRowMapperClass() {
63+
return getMergedAnnotationAttribute("rowMapperClass");
5964
}
60-
65+
6166
/**
6267
* Returns whether the query method is a modifying one.
6368
*
@@ -68,4 +73,10 @@ public boolean isModifyingQuery() {
6873
return AnnotationUtils.findAnnotation(method, Modifying.class) != null;
6974
}
7075

76+
@SuppressWarnings("unchecked")
77+
private <T> T getMergedAnnotationAttribute(String attribute) {
78+
79+
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
80+
return (T) AnnotationUtils.getValue(queryAnnotation, attribute);
81+
}
7182
}

src/main/java/org/springframework/data/jdbc/repository/support/JdbcRepositoryQuery.java

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@
1515
*/
1616
package org.springframework.data.jdbc.repository.support;
1717

18+
import org.springframework.beans.BeanUtils;
1819
import org.springframework.dao.EmptyResultDataAccessException;
1920
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
2021
import org.springframework.data.repository.query.RepositoryQuery;
2122
import org.springframework.jdbc.core.RowMapper;
2223
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
24+
import org.springframework.util.StringUtils;
2325

2426
/**
2527
* 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 {
3638
private final JdbcMappingContext context;
3739
private final RowMapper<?> rowMapper;
3840

39-
JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper rowMapper) {
41+
JdbcRepositoryQuery(JdbcQueryMethod queryMethod, JdbcMappingContext context, RowMapper defaultRowMapper) {
4042

4143
this.queryMethod = queryMethod;
4244
this.context = context;
43-
this.rowMapper = rowMapper;
45+
this.rowMapper = createRowMapper(queryMethod, defaultRowMapper);
46+
}
47+
48+
private static RowMapper<?> createRowMapper(JdbcQueryMethod queryMethod, RowMapper defaultRowMapper) {
49+
50+
Class<?> rowMapperClass = queryMethod.getRowMapperClass();
51+
52+
return rowMapperClass == null || rowMapperClass == RowMapper.class ? defaultRowMapper
53+
: (RowMapper<?>) BeanUtils.instantiateClass(rowMapperClass);
4454
}
4555

4656
@Override
4757
public Object execute(Object[] objects) {
4858

49-
String query = queryMethod.getAnnotatedQuery();
50-
51-
MapSqlParameterSource parameters = new MapSqlParameterSource();
52-
queryMethod.getParameters().getBindableParameters().forEach(p -> {
59+
String query = determineQuery();
5360

54-
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
55-
parameters.addValue(parameterName, objects[p.getIndex()]);
56-
});
61+
MapSqlParameterSource parameters = bindParameters(objects);
5762

5863
if (queryMethod.isModifyingQuery()) {
64+
5965
int updatedCount = context.getTemplate().update(query, parameters);
6066
Class<?> returnedObjectType = queryMethod.getReturnedObjectType();
61-
return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0 : updatedCount;
67+
return (returnedObjectType == boolean.class || returnedObjectType == Boolean.class) ? updatedCount != 0
68+
: updatedCount;
6269
}
6370

6471
if (queryMethod.isCollectionQuery() || queryMethod.isStreamQuery()) {
@@ -76,4 +83,25 @@ public Object execute(Object[] objects) {
7683
public JdbcQueryMethod getQueryMethod() {
7784
return queryMethod;
7885
}
86+
87+
private String determineQuery() {
88+
89+
String query = queryMethod.getAnnotatedQuery();
90+
91+
if (StringUtils.isEmpty(query)) {
92+
throw new IllegalStateException(String.format("No query specified on %s", queryMethod.getName()));
93+
}
94+
return query;
95+
}
96+
97+
private MapSqlParameterSource bindParameters(Object[] objects) {
98+
99+
MapSqlParameterSource parameters = new MapSqlParameterSource();
100+
queryMethod.getParameters().getBindableParameters().forEach(p -> {
101+
102+
String parameterName = p.getName().orElseThrow(() -> new IllegalStateException(PARAMETER_NEEDS_TO_BE_NAMED));
103+
parameters.addValue(parameterName, objects[p.getIndex()]);
104+
});
105+
return parameters;
106+
}
79107
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.support;
17+
18+
import static org.assertj.core.api.Assertions.*;
19+
import static org.mockito.Mockito.*;
20+
21+
import java.lang.reflect.Method;
22+
import java.sql.ResultSet;
23+
24+
import org.junit.Test;
25+
import org.springframework.data.jdbc.repository.query.Query;
26+
import org.springframework.data.projection.ProjectionFactory;
27+
import org.springframework.data.repository.core.RepositoryMetadata;
28+
import org.springframework.jdbc.core.RowMapper;
29+
30+
/**
31+
* Unit tests for {@link JdbcQueryMethod}.
32+
*
33+
* @author Jens Schauder
34+
*/
35+
public class JdbcQueryMethodUnitTests {
36+
37+
public static final String DUMMY_SELECT = "SELECT something";
38+
39+
@Test // DATAJDBC-165
40+
public void returnsSqlStatement() throws NoSuchMethodException {
41+
42+
RepositoryMetadata metadata = mock(RepositoryMetadata.class);
43+
when(metadata.getReturnedDomainClass(any(Method.class))).thenReturn((Class) String.class);
44+
45+
JdbcQueryMethod queryMethod = new JdbcQueryMethod(JdbcQueryMethodUnitTests.class.getDeclaredMethod("queryMethod"),
46+
metadata, mock(ProjectionFactory.class));
47+
48+
assertThat(queryMethod.getAnnotatedQuery()).isEqualTo(DUMMY_SELECT);
49+
}
50+
51+
@Test // DATAJDBC-165
52+
public void returnsSpecifiedRowMapperClass() throws NoSuchMethodException {
53+
54+
RepositoryMetadata metadata = mock(RepositoryMetadata.class);
55+
when(metadata.getReturnedDomainClass(any(Method.class))).thenReturn((Class) String.class);
56+
57+
JdbcQueryMethod queryMethod = new JdbcQueryMethod(JdbcQueryMethodUnitTests.class.getDeclaredMethod("queryMethod"),
58+
metadata, mock(ProjectionFactory.class));
59+
60+
assertThat(queryMethod.getRowMapperClass()).isEqualTo(CustomRowMapper.class);
61+
}
62+
63+
@Query(value = DUMMY_SELECT, rowMapperClass = CustomRowMapper.class)
64+
private void queryMethod() {}
65+
66+
private class CustomRowMapper implements RowMapper {
67+
68+
@Override
69+
public Object mapRow(ResultSet rs, int rowNum) {
70+
return null;
71+
}
72+
}
73+
}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
/*
2+
* Copyright 2018 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.support;
17+
18+
import org.assertj.core.api.Assertions;
19+
import org.junit.Before;
20+
import org.junit.Test;
21+
import org.springframework.data.jdbc.mapping.model.JdbcMappingContext;
22+
import org.springframework.data.repository.query.DefaultParameters;
23+
import org.springframework.data.repository.query.Parameters;
24+
import org.springframework.jdbc.core.RowMapper;
25+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
26+
27+
import java.sql.ResultSet;
28+
29+
import static org.mockito.Mockito.*;
30+
31+
/**
32+
* Unit tests for {@link JdbcRepositoryQuery}.
33+
*
34+
* @author Jens Schauder
35+
*/
36+
public class JdbcRepositoryQueryUnitTests {
37+
38+
JdbcQueryMethod queryMethod;
39+
JdbcMappingContext context;
40+
RowMapper defaultRowMapper;
41+
JdbcRepositoryQuery query;
42+
43+
@Before
44+
public void setup() throws NoSuchMethodException {
45+
46+
Parameters parameters = new DefaultParameters(JdbcRepositoryQueryUnitTests.class.getDeclaredMethod("dummyMethod"));
47+
queryMethod = mock(JdbcQueryMethod.class);
48+
when(queryMethod.getParameters()).thenReturn(parameters);
49+
50+
context = mock(JdbcMappingContext.class, RETURNS_DEEP_STUBS);
51+
defaultRowMapper = mock(RowMapper.class);
52+
}
53+
54+
@Test // DATAJDBC-165
55+
public void emptyQueryThrowsException() {
56+
57+
when(queryMethod.getAnnotatedQuery()).thenReturn(null);
58+
query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper);
59+
60+
Assertions.assertThatExceptionOfType(IllegalStateException.class) //
61+
.isThrownBy(() -> query.execute(new Object[]{}));
62+
}
63+
64+
@Test // DATAJDBC-165
65+
public void defaultRowMapperIsUsedByDefault() {
66+
67+
when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement");
68+
when(queryMethod.getRowMapperClass()).thenReturn((Class) RowMapper.class);
69+
query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper);
70+
71+
query.execute(new Object[]{});
72+
73+
verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper));
74+
}
75+
76+
@Test // DATAJDBC-165
77+
public void defaultRowMapperIsUsedForNull() {
78+
79+
when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement");
80+
query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper);
81+
82+
query.execute(new Object[]{});
83+
84+
verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), eq(defaultRowMapper));
85+
}
86+
87+
@Test // DATAJDBC-165
88+
public void customRowMapperIsUsedWhenSpecified() {
89+
90+
when(queryMethod.getAnnotatedQuery()).thenReturn("some sql statement");
91+
when(queryMethod.getRowMapperClass()).thenReturn((Class) CustomRowMapper.class);
92+
query = new JdbcRepositoryQuery(queryMethod, context, defaultRowMapper);
93+
94+
query.execute(new Object[]{});
95+
96+
verify(context.getTemplate()).queryForObject(anyString(), any(SqlParameterSource.class), isA(CustomRowMapper.class));
97+
}
98+
99+
/**
100+
* The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup.
101+
*/
102+
private void dummyMethod() {
103+
}
104+
105+
private static class CustomRowMapper implements RowMapper {
106+
@Override
107+
public Object mapRow(ResultSet rs, int rowNum) {
108+
return null;
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)