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:
committed by
Jens Schauder
parent
842b5cd176
commit
d191938e59
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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( //
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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> {
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user