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 { @@ -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 { @@ -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));
}

60
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/JdbcCountQueryCreator.java

@ -0,0 +1,60 @@ @@ -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> { @@ -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> { @@ -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> { @@ -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> { @@ -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> { @@ -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> { @@ -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> { @@ -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()) {

115
spring-data-jdbc/src/main/java/org/springframework/data/jdbc/repository/query/PartTreeJdbcQuery.java

@ -16,7 +16,14 @@ @@ -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 @@ -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 { @@ -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 { @@ -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 { @@ -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);
}
}
}

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

@ -73,6 +73,16 @@ public class StringBasedJdbcQuery extends AbstractJdbcQuery { @@ -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( //

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

@ -21,7 +21,9 @@ import org.junit.jupiter.api.Test; @@ -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 { @@ -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;

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

@ -26,10 +26,12 @@ import java.util.concurrent.atomic.AtomicLong; @@ -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 { @@ -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);

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.*; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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> {

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

@ -28,13 +28,15 @@ import java.util.List; @@ -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 { @@ -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

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; @@ -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 { @@ -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

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; @@ -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; @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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> {

18
src/main/asciidoc/jdbc.adoc

@ -491,22 +491,28 @@ interface PersonRepository extends PagingAndSortingRepository<Person, String> { @@ -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
src/main/asciidoc/new-features.adoc

@ -3,6 +3,9 @@ @@ -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

Loading…
Cancel
Save