diff --git a/pom.xml b/pom.xml index b230bbb69e..0b2036ae11 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT pom Spring Data Relational Parent diff --git a/spring-data-jdbc-distribution/pom.xml b/spring-data-jdbc-distribution/pom.xml index a922ef00a2..cc6a151fc2 100644 --- a/spring-data-jdbc-distribution/pom.xml +++ b/spring-data-jdbc-distribution/pom.xml @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT ../pom.xml diff --git a/spring-data-jdbc/pom.xml b/spring-data-jdbc/pom.xml index d00f04e6e3..478fbfeb2d 100644 --- a/spring-data-jdbc/pom.xml +++ b/spring-data-jdbc/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-jdbc - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT Spring Data JDBC Spring Data module for JDBC repositories. @@ -15,7 +15,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java index aa7d16f207..b5ec45aef6 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java @@ -108,7 +108,7 @@ private JdbcQueryExecution createModifyingQueryExecutor() { }; } - private JdbcQueryExecution singleObjectQuery(RowMapper rowMapper) { + JdbcQueryExecution singleObjectQuery(RowMapper rowMapper) { return (query, parameters) -> { try { @@ -119,7 +119,7 @@ private JdbcQueryExecution singleObjectQuery(RowMapper rowMapper) { }; } - private JdbcQueryExecution> collectionQuery(RowMapper rowMapper) { + JdbcQueryExecution> collectionQuery(RowMapper rowMapper) { return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper)); } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java new file mode 100644 index 0000000000..f1557d0565 --- /dev/null +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java @@ -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); + } +} diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java index 64c16652de..67be6aa8b4 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java @@ -66,6 +66,7 @@ class JdbcQueryCreator extends RelationalQueryCreator { 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 { * @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 { this.entityMetadata = entityMetadata; this.queryMapper = new QueryMapper(dialect, converter); this.renderContextFactory = new RenderContextFactory(dialect); + this.isSliceQuery = isSliceQuery; } /** @@ -171,7 +174,7 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) { 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 @@ private SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistent : 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 @@ private SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, R : 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 @@ private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitO 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()) { diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java index bd21b42314..1107214b34 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java @@ -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.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 rowMapper; /** * Creates a new {@link PartTreeJdbcQuery}. @@ -77,7 +87,9 @@ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod query ResultSetExtractor 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 Object execute(Object[] values) { 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>) this.execution, accessor.getPageable()); + } + + if (getQueryMethod().isPageQuery()) { + + return new PageQueryExecution<>((JdbcQueryExecution>) 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 + */ + static class SliceQueryExecution implements JdbcQueryExecution> { + + private final JdbcQueryExecution> delegate; + private final Pageable pageable; + + public SliceQueryExecution(JdbcQueryExecution> delegate, Pageable pageable) { + this.delegate = delegate; + this.pageable = pageable; + } + + @Override + public Slice execute(String query, SqlParameterSource parameter) { + + Collection result = delegate.execute(query, parameter); + + int pageSize = 0; + if (pageable.isPaged()) { + + pageSize = pageable.getPageSize(); + } + + List resultList = result instanceof List ? (List) 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 + */ + static class PageQueryExecution implements JdbcQueryExecution> { + + private final JdbcQueryExecution> delegate; + private final Pageable pageable; + private final LongSupplier countSupplier; + + public PageQueryExecution(JdbcQueryExecution> delegate, Pageable pageable, + LongSupplier countSupplier) { + this.delegate = delegate; + this.pageable = pageable; + this.countSupplier = countSupplier; + } + + @Override + public Slice execute(String query, SqlParameterSource parameter) { + + Collection result = delegate.execute(query, parameter); + + return PageableExecutionUtils.getPage(result instanceof List ? (List) result : new ArrayList<>(result), + pageable, countSupplier); + } + + } } diff --git a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java index 243f36dd62..365a8055c0 100644 --- a/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java +++ b/spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java @@ -73,6 +73,16 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera 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 rowMapper = determineRowMapper(defaultRowMapper); return getQueryExecution( // diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java index 3e1e166585..20c733ad41 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java @@ -21,7 +21,9 @@ 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; diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java index 6b51d7354c..31ac6a93a0 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java @@ -26,10 +26,12 @@ 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 @@ static class ImmutableWithManualIdEntity { @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); diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java index 16f398821f..5b8cd32fe1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java @@ -21,17 +21,19 @@ 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.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 void usePrimitiveArrayAsArgument() { 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 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 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 { List findAllByNamedQuery(); @@ -432,6 +469,10 @@ interface DummyEntityRepository extends CrudRepository { @Query("select unnest( :ids )") List unnestPrimitive(@Param("ids") int[] ids); + + Page findPageByNameContains(String name, Pageable pageable); + + Slice findSliceByNameContains(String name, Pageable pageable); } @Configuration @@ -476,10 +517,15 @@ public void onApplicationEvent(AbstractRelationalEvent event) { } @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 { diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java index b970c77433..4e1c5c3dd1 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java @@ -28,13 +28,15 @@ 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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java index 01b96b50ab..f6abbbe705 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java @@ -30,7 +30,9 @@ 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 diff --git a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java index c0821a81ba..0d72eca896 100644 --- a/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java +++ b/spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java @@ -18,23 +18,31 @@ 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 @@ */ public class StringBasedJdbcQueryUnitTests { - JdbcQueryMethod queryMethod; RowMapper 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 void setup() throws NoSuchMethodException { @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 void defaultRowMapperIsUsedForNull() { @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 void customRowMapperIsUsedWhenSpecified() { @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 resultSetExtractor = query.determineResultSetExtractor(defaultRowMapper); @@ -136,11 +115,8 @@ public void customResultSetExtractorIsUsedWhenSpecified() { @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 resultSetExtractor = query .determineResultSetExtractor(query.determineRowMapper(defaultRowMapper)); @@ -151,11 +127,61 @@ public void customResultSetExtractorAndRowMapperGetCombined() { "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 { + + @Query(value = "some sql statement") + List findAll(); + + @Query(value = "some sql statement", rowMapperClass = CustomRowMapper.class) + List findAllWithCustomRowMapper(); + + @Query(value = "some sql statement", resultSetExtractorClass = CustomResultSetExtractor.class) + List findAllWithCustomResultSetExtractor(); + + @Query(value = "some sql statement", rowMapperClass = CustomRowMapper.class, + resultSetExtractorClass = CustomResultSetExtractor.class) + List findAllWithCustomRowMapperAndResultSetExtractor(); + + List noAnnotation(); + + @Query(value = "some sql statement") + Page pageAll(Pageable pageable); + + @Query(value = "some sql statement") + Slice sliceAll(Pageable pageable); + + } private static class CustomRowMapper implements RowMapper { diff --git a/spring-data-relational/pom.xml b/spring-data-relational/pom.xml index 6b2507aabe..a51ea3fe8b 100644 --- a/spring-data-relational/pom.xml +++ b/spring-data-relational/pom.xml @@ -6,7 +6,7 @@ 4.0.0 spring-data-relational - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT Spring Data Relational Spring Data Relational support @@ -14,7 +14,7 @@ org.springframework.data spring-data-relational-parent - 2.2.0-SNAPSHOT + 2.2.0-GH-774-SNAPSHOT diff --git a/src/main/asciidoc/jdbc.adoc b/src/main/asciidoc/jdbc.adoc index 7f8403ecde..2ee52175a2 100644 --- a/src/main/asciidoc/jdbc.adoc +++ b/src/main/asciidoc/jdbc.adoc @@ -491,22 +491,28 @@ interface PersonRepository extends PagingAndSortingRepository { List findByFirstnameOrderByLastname(String firstname, Pageable pageable); <2> - Person findByFirstnameAndLastname(String firstname, String lastname); <3> + Slice findByLastname(String lastname, Pageable pageable); <3> - Person findFirstByLastname(String lastname); <4> + Page 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 findByLastname(String lastname); <5> + List 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`. 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`. 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: diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 70961eb2ca..293d155c44 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -3,6 +3,9 @@ This section covers the significant changes for each version. +[[new-features.2-2-0]] +== `Page` and `Slice` support for <>. + [[new-features.2-1-0]] == What's New in Spring Data JDBC 2.1