Add support for Slice and Page queries using query derivation.

We now support pagination for queries returning Slice and Page.

interface PersonRepository extends PagingAndSortingRepository<Person, String> {

  Slice<Person> findFirstByLastname(String lastname, Pageable pageable);

  Page<Person> findFirstByLastname(String lastname, Pageable pageable);
}

Closes #774
Original pull request #952
This commit is contained in:
Mark Paluch
2021-03-30 11:10:40 +02:00
committed by Jens Schauder
parent 842b5cd176
commit d191938e59
13 changed files with 346 additions and 74 deletions

View File

@@ -108,7 +108,7 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery {
};
}
private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
return (query, parameters) -> {
try {
@@ -119,7 +119,7 @@ public abstract class AbstractJdbcQuery implements RepositoryQuery {
};
}
private <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
<T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper));
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2021 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
*
* https://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.query;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.relational.core.dialect.Dialect;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
import org.springframework.data.relational.core.sql.Expressions;
import org.springframework.data.relational.core.sql.Functions;
import org.springframework.data.relational.core.sql.Select;
import org.springframework.data.relational.core.sql.SelectBuilder;
import org.springframework.data.relational.core.sql.Table;
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
import org.springframework.data.repository.query.parser.PartTree;
/**
* {@link JdbcQueryCreator} that creates {@code COUNT(*)} queries without applying limit/offset and {@link Sort}.
*
* @author Mark Paluch
* @since 2.2
*/
class JdbcCountQueryCreator extends JdbcQueryCreator {
JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery);
}
@Override
SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
SelectBuilder.SelectOrdered selectOrdered) {
return selectOrdered;
}
@Override
SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
}
@Override
SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
return Select.builder().select(Functions.count(Expressions.asterisk())).from(table);
}
}

View File

@@ -66,6 +66,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
private final QueryMapper queryMapper;
private final RelationalEntityMetadata<?> entityMetadata;
private final RenderContextFactory renderContextFactory;
private final boolean isSliceQuery;
/**
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
@@ -77,9 +78,10 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
* @param dialect must not be {@literal null}.
* @param entityMetadata relational entity metadata, must not be {@literal null}.
* @param accessor parameter metadata provider, must not be {@literal null}.
* @param isSliceQuery
*/
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
super(tree, accessor);
Assert.notNull(converter, "JdbcConverter must not be null");
@@ -93,6 +95,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
this.entityMetadata = entityMetadata;
this.queryMapper = new QueryMapper(dialect, converter);
this.renderContextFactory = new RenderContextFactory(dialect);
this.isSliceQuery = isSliceQuery;
}
/**
@@ -171,7 +174,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
return new ParametrizedQuery(sql, parameterSource);
}
private SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
SelectBuilder.SelectOrdered selectOrdered) {
return sort.isSorted() ? //
@@ -179,7 +182,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
: selectOrdered;
}
private SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, RelationalPersistentEntity<?> entity,
SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, RelationalPersistentEntity<?> entity,
Table table, MapSqlParameterSource parameterSource, SelectBuilder.SelectWhere whereBuilder) {
return criteria != null //
@@ -187,7 +190,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
: whereBuilder;
}
private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
if (tree.isExistsProjection()) {
limitOffsetBuilder = limitOffsetBuilder.limit(1);
@@ -197,13 +200,14 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
Pageable pageable = accessor.getPageable();
if (pageable.isPaged()) {
limitOffsetBuilder = limitOffsetBuilder.limit(pageable.getPageSize()).offset(pageable.getOffset());
limitOffsetBuilder = limitOffsetBuilder.limit(isSliceQuery ? pageable.getPageSize() + 1 : pageable.getPageSize())
.offset(pageable.getOffset());
}
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
}
private SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
SelectBuilder.SelectJoin builder;
if (tree.isExistsProjection()) {

View File

@@ -16,7 +16,14 @@
package org.springframework.data.jdbc.repository.query;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.function.LongSupplier;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.SliceImpl;
import org.springframework.data.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.relational.core.dialect.Dialect;
@@ -26,9 +33,11 @@ import org.springframework.data.relational.repository.query.RelationalParameterA
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.util.Assert;
/**
@@ -46,6 +55,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
private final JdbcConverter converter;
private final PartTree tree;
private final JdbcQueryExecution<?> execution;
private final RowMapper<Object> rowMapper;
/**
* Creates a new {@link PartTreeJdbcQuery}.
@@ -77,7 +87,9 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
ResultSetExtractor<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;
this.execution = getQueryExecution(queryMethod, extractor, rowMapper);
this.execution = queryMethod.isPageQuery() || queryMethod.isSliceQuery() ? collectionQuery(rowMapper)
: getQueryExecution(queryMethod, extractor, rowMapper);
this.rowMapper = rowMapper;
}
private Sort getDynamicSort(RelationalParameterAccessor accessor) {
@@ -93,15 +105,108 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
values);
ParametrizedQuery query = createQuery(accessor);
return this.execution.execute(query.getQuery(), query.getParameterSource());
JdbcQueryExecution<?> execution = getQueryExecution(accessor);
return execution.execute(query.getQuery(), query.getParameterSource());
}
private JdbcQueryExecution<?> getQueryExecution(RelationalParametersParameterAccessor accessor) {
if (getQueryMethod().isSliceQuery()) {
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.execution, accessor.getPageable());
}
if (getQueryMethod().isPageQuery()) {
return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.execution, accessor.getPageable(),
() -> {
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
entityMetadata, accessor, false);
ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted());
Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(),
countQuery.getParameterSource());
return converter.getConversionService().convert(count, Long.class);
});
}
return this.execution;
}
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor);
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
getQueryMethod().isSliceQuery());
return queryCreator.createQuery(getDynamicSort(accessor));
}
/**
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
*
* @param <T>
*/
static class SliceQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
private final JdbcQueryExecution<? extends Collection<T>> delegate;
private final Pageable pageable;
public SliceQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable) {
this.delegate = delegate;
this.pageable = pageable;
}
@Override
public Slice<T> execute(String query, SqlParameterSource parameter) {
Collection<T> result = delegate.execute(query, parameter);
int pageSize = 0;
if (pageable.isPaged()) {
pageSize = pageable.getPageSize();
}
List<T> resultList = result instanceof List ? (List<T>) result : new ArrayList<>(result);
boolean hasNext = pageable.isPaged() && resultList.size() > pageSize;
return new SliceImpl<>(hasNext ? resultList.subList(0, pageSize) : resultList, pageable, hasNext);
}
}
/**
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Page}.
*
* @param <T>
*/
static class PageQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
private final JdbcQueryExecution<? extends Collection<T>> delegate;
private final Pageable pageable;
private final LongSupplier countSupplier;
public PageQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable,
LongSupplier countSupplier) {
this.delegate = delegate;
this.pageable = pageable;
this.countSupplier = countSupplier;
}
@Override
public Slice<T> execute(String query, SqlParameterSource parameter) {
Collection<T> result = delegate.execute(query, parameter);
return PageableExecutionUtils.getPage(result instanceof List ? (List<T>) result : new ArrayList<>(result),
pageable, countSupplier);
}
}
}

View File

@@ -73,6 +73,16 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
this.queryMethod = queryMethod;
this.converter = converter;
if (queryMethod.isSliceQuery()) {
throw new UnsupportedOperationException(
"Slice queries are not supported using string-based queries. Offending method: " + queryMethod);
}
if (queryMethod.isPageQuery()) {
throw new UnsupportedOperationException(
"Page queries are not supported using string-based queries. Offending method: " + queryMethod);
}
executor = Lazy.of(() -> {
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
return getQueryExecution( //

View File

@@ -21,7 +21,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
@@ -55,7 +57,8 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
@EnableJdbcRepositories(considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(value = Ones.class, type = FilterType.ASSIGNABLE_TYPE))
static class Config {
@Autowired JdbcRepositoryFactory factory;

View File

@@ -26,10 +26,12 @@ import java.util.concurrent.atomic.AtomicLong;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
@@ -141,7 +143,8 @@ public class JdbcRepositoryIdGenerationIntegrationTests {
@Configuration
@ComponentScan("org.springframework.data.jdbc.testing")
@EnableJdbcRepositories(considerNestedRepositories = true)
@EnableJdbcRepositories(considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(value = CrudRepository.class, type = FilterType.ASSIGNABLE_TYPE))
static class TestConfiguration {
AtomicLong lastId = new AtomicLong(0);

View File

@@ -21,17 +21,19 @@ import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException;
import java.sql.ResultSet;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import lombok.ToString;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationListener;
@@ -40,6 +42,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Id;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.repository.query.Query;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener;
@@ -404,6 +410,37 @@ public class JdbcRepositoryIntegrationTests {
assertThat(repository.unnestPrimitive(new int[]{1, 2, 3})).containsExactly(1,2,3);
}
@Test // GH-774
public void pageByNameShouldReturnCorrectResult() {
repository.saveAll(Arrays.asList(new DummyEntity("a1"), new DummyEntity("a2"), new DummyEntity("a3")));
Page<DummyEntity> page = repository.findPageByNameContains("a", PageRequest.of(0, 5));
assertThat(page.getContent()).hasSize(3);
assertThat(page.getTotalElements()).isEqualTo(3);
assertThat(page.getTotalPages()).isEqualTo(1);
assertThat(repository.findPageByNameContains("a", PageRequest.of(0, 2)).getContent()).hasSize(2);
assertThat(repository.findPageByNameContains("a", PageRequest.of(1, 2)).getContent()).hasSize(1);
}
@Test // GH-774
public void sliceByNameShouldReturnCorrectResult() {
repository.saveAll(Arrays.asList(new DummyEntity("a1"), new DummyEntity("a2"), new DummyEntity("a3")));
Slice<DummyEntity> slice = repository.findSliceByNameContains("a", PageRequest.of(0, 5));
assertThat(slice.getContent()).hasSize(3);
assertThat(slice.hasNext()).isFalse();
slice = repository.findSliceByNameContains("a", PageRequest.of(0, 2));
assertThat(slice.getContent()).hasSize(2);
assertThat(slice.hasNext()).isTrue();
}
interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
List<DummyEntity> findAllByNamedQuery();
@@ -432,6 +469,10 @@ public class JdbcRepositoryIntegrationTests {
@Query("select unnest( :ids )")
List<Integer> unnestPrimitive(@Param("ids") int[] ids);
Page<DummyEntity> findPageByNameContains(String name, Pageable pageable);
Slice<DummyEntity> findSliceByNameContains(String name, Pageable pageable);
}
@Configuration
@@ -476,10 +517,15 @@ public class JdbcRepositoryIntegrationTests {
}
@Data
@NoArgsConstructor
static class DummyEntity {
String name;
Instant pointInTime;
@Id private Long idProp;
public DummyEntity(String name) {
this.name = name;
}
}
static class CustomRowMapper implements RowMapper<DummyEntity> {

View File

@@ -28,13 +28,15 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.convert.EntityRowMapper;
import org.springframework.data.jdbc.repository.config.DefaultQueryMappingConfiguration;
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
import org.springframework.data.jdbc.repository.query.Query;
@@ -63,7 +65,8 @@ public class StringBasedJdbcQueryMappingConfigurationIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
@EnableJdbcRepositories(considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(value = CarRepository.class, type = FilterType.ASSIGNABLE_TYPE))
static class Config {
@Bean

View File

@@ -30,7 +30,9 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.dao.DataAccessException;
import org.springframework.data.annotation.Id;
@@ -57,7 +59,8 @@ public class QueryAnnotationHsqlIntegrationTests {
@Configuration
@Import(TestConfiguration.class)
@EnableJdbcRepositories(considerNestedRepositories = true)
@EnableJdbcRepositories(considerNestedRepositories = true,
includeFilters = @ComponentScan.Filter(value = DummyEntityRepository.class, type = FilterType.ASSIGNABLE_TYPE))
static class Config {
@Bean

View File

@@ -18,23 +18,31 @@ package org.springframework.data.jdbc.repository.query;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import java.lang.reflect.Method;
import java.sql.ResultSet;
import java.util.List;
import java.util.Properties;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Slice;
import org.springframework.data.jdbc.core.convert.BasicJdbcConverter;
import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.jdbc.core.convert.RelationResolver;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
import org.springframework.data.relational.repository.query.RelationalParameters;
import org.springframework.data.repository.query.DefaultParameters;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.core.support.DefaultRepositoryMetadata;
import org.springframework.data.repository.core.support.PropertiesBasedNamedQueries;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.util.ReflectionUtils;
/**
* Unit tests for {@link StringBasedJdbcQuery}.
@@ -47,7 +55,6 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
*/
public class StringBasedJdbcQueryUnitTests {
JdbcQueryMethod queryMethod;
RowMapper<Object> defaultRowMapper;
NamedParameterJdbcOperations operations;
@@ -57,12 +64,6 @@ public class StringBasedJdbcQueryUnitTests {
@BeforeEach
public void setup() throws NoSuchMethodException {
this.queryMethod = mock(JdbcQueryMethod.class);
Parameters<?, ?> parameters = new RelationalParameters(
StringBasedJdbcQueryUnitTests.class.getDeclaredMethod("dummyMethod"));
doReturn(parameters).when(queryMethod).getParameters();
this.defaultRowMapper = mock(RowMapper.class);
this.operations = mock(NamedParameterJdbcOperations.class);
this.context = mock(RelationalMappingContext.class, RETURNS_DEEP_STUBS);
@@ -72,34 +73,18 @@ public class StringBasedJdbcQueryUnitTests {
@Test // DATAJDBC-165
public void emptyQueryThrowsException() {
doReturn(null).when(queryMethod).getDeclaredQuery();
JdbcQueryMethod queryMethod = createMethod("noAnnotation");
Assertions.assertThatExceptionOfType(IllegalStateException.class) //
.isThrownBy(() -> createQuery()
.isThrownBy(() -> createQuery(queryMethod)
.execute(new Object[] {}));
}
private StringBasedJdbcQuery createQuery() {
StringBasedJdbcQuery query = new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
return query;
}
@Test // DATAJDBC-165
public void defaultRowMapperIsUsedByDefault() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(RowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = createQuery();
assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper);
}
@Test // DATAJDBC-165, DATAJDBC-318
public void defaultRowMapperIsUsedForNull() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
StringBasedJdbcQuery query = createQuery();
JdbcQueryMethod queryMethod = createMethod("findAll");
StringBasedJdbcQuery query = createQuery(queryMethod);
assertThat(query.determineRowMapper(defaultRowMapper)).isEqualTo(defaultRowMapper);
}
@@ -107,10 +92,8 @@ public class StringBasedJdbcQueryUnitTests {
@Test // DATAJDBC-165, DATAJDBC-318
public void customRowMapperIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = createQuery();
JdbcQueryMethod queryMethod = createMethod("findAllWithCustomRowMapper");
StringBasedJdbcQuery query = createQuery(queryMethod);
assertThat(query.determineRowMapper(defaultRowMapper)).isInstanceOf(CustomRowMapper.class);
}
@@ -118,12 +101,8 @@ public class StringBasedJdbcQueryUnitTests {
@Test // DATAJDBC-290
public void customResultSetExtractorIsUsedWhenSpecified() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
createQuery().execute(new Object[] {});
StringBasedJdbcQuery query = createQuery();
JdbcQueryMethod queryMethod = createMethod("findAllWithCustomResultSetExtractor");
StringBasedJdbcQuery query = createQuery(queryMethod);
ResultSetExtractor<Object> resultSetExtractor = query.determineResultSetExtractor(defaultRowMapper);
@@ -136,11 +115,8 @@ public class StringBasedJdbcQueryUnitTests {
@Test // DATAJDBC-290
public void customResultSetExtractorAndRowMapperGetCombined() {
doReturn("some sql statement").when(queryMethod).getDeclaredQuery();
doReturn(CustomResultSetExtractor.class).when(queryMethod).getResultSetExtractorClass();
doReturn(CustomRowMapper.class).when(queryMethod).getRowMapperClass();
StringBasedJdbcQuery query = createQuery();
JdbcQueryMethod queryMethod = createMethod("findAllWithCustomRowMapperAndResultSetExtractor");
StringBasedJdbcQuery query = createQuery(queryMethod);
ResultSetExtractor<Object> resultSetExtractor = query
.determineResultSetExtractor(query.determineRowMapper(defaultRowMapper));
@@ -151,11 +127,61 @@ public class StringBasedJdbcQueryUnitTests {
"RowMapper is not expected to be custom");
}
/**
* The whole purpose of this method is to easily generate a {@link DefaultParameters} instance during test setup.
*/
@SuppressWarnings("unused")
private void dummyMethod() {}
@Test // GH-774
public void sliceQueryNotSupported() {
JdbcQueryMethod queryMethod = createMethod("sliceAll", Pageable.class);
assertThatThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Slice queries are not supported using string-based queries");
}
@Test // GH-774
public void pageQueryNotSupported() {
JdbcQueryMethod queryMethod = createMethod("pageAll", Pageable.class);
assertThatThrownBy(() -> new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter))
.isInstanceOf(UnsupportedOperationException.class)
.hasMessageContaining("Page queries are not supported using string-based queries");
}
private JdbcQueryMethod createMethod(String methodName, Class<?>... paramTypes) {
Method method = ReflectionUtils.findMethod(MyRepository.class, methodName, paramTypes);
return new JdbcQueryMethod(method, new DefaultRepositoryMetadata(MyRepository.class),
new SpelAwareProxyProjectionFactory(), new PropertiesBasedNamedQueries(new Properties()), this.context);
}
private StringBasedJdbcQuery createQuery(JdbcQueryMethod queryMethod) {
return new StringBasedJdbcQuery(queryMethod, operations, defaultRowMapper, converter);
}
interface MyRepository extends Repository<Object, Long> {
@Query(value = "some sql statement")
List<Object> findAll();
@Query(value = "some sql statement", rowMapperClass = CustomRowMapper.class)
List<Object> findAllWithCustomRowMapper();
@Query(value = "some sql statement", resultSetExtractorClass = CustomResultSetExtractor.class)
List<Object> findAllWithCustomResultSetExtractor();
@Query(value = "some sql statement", rowMapperClass = CustomRowMapper.class,
resultSetExtractorClass = CustomResultSetExtractor.class)
List<Object> findAllWithCustomRowMapperAndResultSetExtractor();
List<Object> noAnnotation();
@Query(value = "some sql statement")
Page<Object> pageAll(Pageable pageable);
@Query(value = "some sql statement")
Slice<Object> sliceAll(Pageable pageable);
}
private static class CustomRowMapper implements RowMapper<Object> {

View File

@@ -491,22 +491,28 @@ interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); <2>
Person findByFirstnameAndLastname(String firstname, String lastname); <3>
Slice<Person> findByLastname(String lastname, Pageable pageable); <3>
Person findFirstByLastname(String lastname); <4>
Page<Person> findByLastname(String lastname, Pageable pageable); <4>
Person findByFirstnameAndLastname(String firstname, String lastname); <5>
Person findFirstByLastname(String lastname); <6>
@Query("SELECT * FROM person WHERE lastname = :lastname")
List<Person> findByLastname(String lastname); <5>
List<Person> findByLastname(String lastname); <7>
}
----
<1> The method shows a query for all people with the given `lastname`.
The query is derived by parsing the method name for constraints that can be concatenated with `And` and `Or`.
Thus, the method name results in a query expression of `SELECT … FROM person WHERE firstname = :firstname`.
<2> Use `Pageable` to pass offset and sorting parameters to the database.
<3> Find a single entity for the given criteria.
<3> Return a `Slice<Person>`. Selects `LIMIT+1` rows to determine whether there's more data to consume. `ResultSetExtractor` customization is not supported.
<4> Run a paginated query returning `Page<Person>`. Selects only data within the given page bounds and potentially a count query to determine the total count. `ResultSetExtractor` customization is not supported.
<5> Find a single entity for the given criteria.
It completes with `IncorrectResultSizeDataAccessException` on non-unique results.
<4> In contrast to <3>, the first entity is always emitted even if the query yields more result documents.
<5> The `findByLastname` method shows a query for all people with the given last name.
<6> In contrast to <3>, the first entity is always emitted even if the query yields more result documents.
<7> The `findByLastname` method shows a query for all people with the given last name.
====
The following table shows the keywords that are supported for query methods:

View File

@@ -3,6 +3,9 @@
This section covers the significant changes for each version.
[[new-features.2-2-0]]
== `Page` and `Slice` support for <<jdbc.query-methods,derived queries>>.
[[new-features.2-1-0]]
== What's New in Spring Data JDBC 2.1