Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jdbc</artifactId>
<version>1.0.0.BUILD-SNAPSHOT</version>
<version>1.0.0.DATAJDBC-165-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand All @@ -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.
*
Expand All @@ -68,4 +73,10 @@ public boolean isModifyingQuery() {
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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()) {
Expand All @@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
}