Browse Source

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
pull/961/head
Mark Paluch 5 years ago committed by Jens Schauder
parent
commit
d191938e59
No known key found for this signature in database
GPG Key ID: 45CC872F17423DBF
  1. 4
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java
  2. 60
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java
  3. 16
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java
  4. 115
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java
  5. 10
      spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java
  6. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java
  7. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java
  8. 48
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java
  9. 7
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java
  10. 5
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java
  11. 126
      spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java
  12. 18
      src/main/asciidoc/jdbc.adoc
  13. 3
      src/main/asciidoc/new-features.adoc

4
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/AbstractJdbcQuery.java

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

60
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);
}
}

16
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcQueryCreator.java

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

115
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; package org.springframework.data.jdbc.repository.query;
import java.sql.ResultSet; 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.domain.Sort;
import org.springframework.data.jdbc.core.convert.JdbcConverter; import org.springframework.data.jdbc.core.convert.JdbcConverter;
import org.springframework.data.relational.core.dialect.Dialect; 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.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.parser.PartTree; 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.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations; import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -46,6 +55,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
private final JdbcConverter converter; private final JdbcConverter converter;
private final PartTree tree; private final PartTree tree;
private final JdbcQueryExecution<?> execution; private final JdbcQueryExecution<?> execution;
private final RowMapper<Object> rowMapper;
/** /**
* Creates a new {@link PartTreeJdbcQuery}. * Creates a new {@link PartTreeJdbcQuery}.
@ -77,7 +87,9 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
ResultSetExtractor<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null; 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) { private Sort getDynamicSort(RelationalParameterAccessor accessor) {
@ -93,15 +105,108 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(), RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
values); values);
ParametrizedQuery query = createQuery(accessor); 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) { protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation(); 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)); 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);
}
}
} }

10
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQuery.java

@ -73,6 +73,16 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery {
this.queryMethod = queryMethod; this.queryMethod = queryMethod;
this.converter = converter; 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(() -> { executor = Lazy.of(() -> {
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper); RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
return getQueryExecution( // return getQueryExecution( //

5
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryCrossAggregateHsqlIntegrationTests.java

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

5
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIdGenerationIntegrationTests.java

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

48
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/JdbcRepositoryIntegrationTests.java

@ -21,17 +21,19 @@ import static org.assertj.core.api.SoftAssertions.*;
import static org.springframework.test.context.TestExecutionListeners.MergeMode.*; import static org.springframework.test.context.TestExecutionListeners.MergeMode.*;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException; import java.io.IOException;
import java.sql.ResultSet; import java.sql.ResultSet;
import java.time.Instant; import java.time.Instant;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import lombok.ToString;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.PropertiesFactoryBean; import org.springframework.beans.factory.config.PropertiesFactoryBean;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
@ -40,6 +42,10 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.ClassPathResource;
import org.springframework.data.annotation.Id; 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.query.Query;
import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory; import org.springframework.data.jdbc.repository.support.JdbcRepositoryFactory;
import org.springframework.data.jdbc.testing.AssumeFeatureTestExecutionListener; 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); 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> { interface DummyEntityRepository extends CrudRepository<DummyEntity, Long> {
List<DummyEntity> findAllByNamedQuery(); List<DummyEntity> findAllByNamedQuery();
@ -432,6 +469,10 @@ public class JdbcRepositoryIntegrationTests {
@Query("select unnest( :ids )") @Query("select unnest( :ids )")
List<Integer> unnestPrimitive(@Param("ids") int[] ids); List<Integer> unnestPrimitive(@Param("ids") int[] ids);
Page<DummyEntity> findPageByNameContains(String name, Pageable pageable);
Slice<DummyEntity> findSliceByNameContains(String name, Pageable pageable);
} }
@Configuration @Configuration
@ -476,10 +517,15 @@ public class JdbcRepositoryIntegrationTests {
} }
@Data @Data
@NoArgsConstructor
static class DummyEntity { static class DummyEntity {
String name; String name;
Instant pointInTime; Instant pointInTime;
@Id private Long idProp; @Id private Long idProp;
public DummyEntity(String name) {
this.name = name;
}
} }
static class CustomRowMapper implements RowMapper<DummyEntity> { static class CustomRowMapper implements RowMapper<DummyEntity> {

7
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/StringBasedJdbcQueryMappingConfigurationIntegrationTests.java

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

5
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/QueryAnnotationHsqlIntegrationTests.java

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

126
spring-data-jdbc/src/test/java/org/springframework/data/jdbc/repository/query/StringBasedJdbcQueryUnitTests.java

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

18
src/main/asciidoc/jdbc.adoc

@ -491,22 +491,28 @@ interface PersonRepository extends PagingAndSortingRepository<Person, String> {
List<Person> findByFirstnameOrderByLastname(String firstname, Pageable pageable); <2> 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") @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`. <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`. 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`. 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. <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. 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. <6> 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. <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: The following table shows the keywords that are supported for query methods:

3
src/main/asciidoc/new-features.adoc

@ -3,6 +3,9 @@
This section covers the significant changes for each version. 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]] [[new-features.2-1-0]]
== What's New in Spring Data JDBC 2.1 == What's New in Spring Data JDBC 2.1

Loading…
Cancel
Save