DATAJDBC-165 - Allows configuration of custom RowMapper on @Query annotation.
This commit is contained in:
committed by
Greg Turnquist
parent
5f6a44d00a
commit
043bd4de35
@@ -22,6 +22,7 @@ import java.lang.annotation.RetentionPolicy;
|
||||
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 @@ import org.springframework.data.annotation.QueryAnnotation;
|
||||
@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;
|
||||
}
|
||||
|
||||
@@ -28,9 +28,7 @@ import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* {@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 class JdbcQueryMethod extends QueryMethod {
|
||||
*/
|
||||
@Nullable
|
||||
public String getAnnotatedQuery() {
|
||||
|
||||
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
|
||||
|
||||
return queryAnnotation == null ? null : queryAnnotation.value();
|
||||
return getMergedAnnotationAttribute("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 class JdbcQueryMethod extends QueryMethod {
|
||||
return AnnotationUtils.findAnnotation(method, Modifying.class) != null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> T getMergedAnnotationAttribute(String attribute) {
|
||||
|
||||
Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class);
|
||||
return (T) AnnotationUtils.getValue(queryAnnotation, attribute);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
String query = determineQuery();
|
||||
|
||||
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()]);
|
||||
});
|
||||
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 @@ class JdbcRepositoryQuery implements RepositoryQuery {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user