Skip to content

Commit d191938

Browse files
mp911deschauder
authored andcommitted
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
1 parent 842b5cd commit d191938

13 files changed

+347
-75
lines changed

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
108108
};
109109
}
110110

111-
private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
111+
JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
112112

113113
return (query, parameters) -> {
114114
try {
@@ -119,7 +119,7 @@ private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
119119
};
120120
}
121121

122-
private <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
122+
<T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
123123
return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper));
124124
}
125125

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2021 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.jdbc.repository.query;
17+
18+
import org.springframework.data.domain.Sort;
19+
import org.springframework.data.jdbc.core.convert.JdbcConverter;
20+
import org.springframework.data.relational.core.dialect.Dialect;
21+
import org.springframework.data.relational.core.mapping.RelationalMappingContext;
22+
import org.springframework.data.relational.core.mapping.RelationalPersistentEntity;
23+
import org.springframework.data.relational.core.sql.Expressions;
24+
import org.springframework.data.relational.core.sql.Functions;
25+
import org.springframework.data.relational.core.sql.Select;
26+
import org.springframework.data.relational.core.sql.SelectBuilder;
27+
import org.springframework.data.relational.core.sql.Table;
28+
import org.springframework.data.relational.repository.query.RelationalEntityMetadata;
29+
import org.springframework.data.relational.repository.query.RelationalParameterAccessor;
30+
import org.springframework.data.repository.query.parser.PartTree;
31+
32+
/**
33+
* {@link JdbcQueryCreator} that creates {@code COUNT(*)} queries without applying limit/offset and {@link Sort}.
34+
*
35+
* @author Mark Paluch
36+
* @since 2.2
37+
*/
38+
class JdbcCountQueryCreator extends JdbcQueryCreator {
39+
40+
JdbcCountQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
41+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
42+
super(context, tree, converter, dialect, entityMetadata, accessor, isSliceQuery);
43+
}
44+
45+
@Override
46+
SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
47+
SelectBuilder.SelectOrdered selectOrdered) {
48+
return selectOrdered;
49+
}
50+
51+
@Override
52+
SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
53+
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
54+
}
55+
56+
@Override
57+
SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
58+
return Select.builder().select(Functions.count(Expressions.asterisk())).from(table);
59+
}
60+
}

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
6666
private final QueryMapper queryMapper;
6767
private final RelationalEntityMetadata<?> entityMetadata;
6868
private final RenderContextFactory renderContextFactory;
69+
private final boolean isSliceQuery;
6970

7071
/**
7172
* Creates new instance of this class with the given {@link PartTree}, {@link JdbcConverter}, {@link Dialect},
@@ -77,9 +78,10 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
7778
* @param dialect must not be {@literal null}.
7879
* @param entityMetadata relational entity metadata, must not be {@literal null}.
7980
* @param accessor parameter metadata provider, must not be {@literal null}.
81+
* @param isSliceQuery
8082
*/
8183
JdbcQueryCreator(RelationalMappingContext context, PartTree tree, JdbcConverter converter, Dialect dialect,
82-
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor) {
84+
RelationalEntityMetadata<?> entityMetadata, RelationalParameterAccessor accessor, boolean isSliceQuery) {
8385
super(tree, accessor);
8486

8587
Assert.notNull(converter, "JdbcConverter must not be null");
@@ -93,6 +95,7 @@ class JdbcQueryCreator extends RelationalQueryCreator<ParametrizedQuery> {
9395
this.entityMetadata = entityMetadata;
9496
this.queryMapper = new QueryMapper(dialect, converter);
9597
this.renderContextFactory = new RenderContextFactory(dialect);
98+
this.isSliceQuery = isSliceQuery;
9699
}
97100

98101
/**
@@ -171,23 +174,23 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
171174
return new ParametrizedQuery(sql, parameterSource);
172175
}
173176

174-
private SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
177+
SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
175178
SelectBuilder.SelectOrdered selectOrdered) {
176179

177180
return sort.isSorted() ? //
178181
selectOrdered.orderBy(queryMapper.getMappedSort(table, sort, entity)) //
179182
: selectOrdered;
180183
}
181184

182-
private SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, RelationalPersistentEntity<?> entity,
185+
SelectBuilder.SelectOrdered applyCriteria(@Nullable Criteria criteria, RelationalPersistentEntity<?> entity,
183186
Table table, MapSqlParameterSource parameterSource, SelectBuilder.SelectWhere whereBuilder) {
184187

185188
return criteria != null //
186189
? whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)) //
187190
: whereBuilder;
188191
}
189192

190-
private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
193+
SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
191194

192195
if (tree.isExistsProjection()) {
193196
limitOffsetBuilder = limitOffsetBuilder.limit(1);
@@ -197,13 +200,14 @@ private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitO
197200

198201
Pageable pageable = accessor.getPageable();
199202
if (pageable.isPaged()) {
200-
limitOffsetBuilder = limitOffsetBuilder.limit(pageable.getPageSize()).offset(pageable.getOffset());
203+
limitOffsetBuilder = limitOffsetBuilder.limit(isSliceQuery ? pageable.getPageSize() + 1 : pageable.getPageSize())
204+
.offset(pageable.getOffset());
201205
}
202206

203207
return (SelectBuilder.SelectWhere) limitOffsetBuilder;
204208
}
205209

206-
private SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
210+
SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
207211

208212
SelectBuilder.SelectJoin builder;
209213
if (tree.isExistsProjection()) {

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

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,14 @@
1616
package org.springframework.data.jdbc.repository.query;
1717

1818
import java.sql.ResultSet;
19-
19+
import java.util.ArrayList;
20+
import java.util.Collection;
21+
import java.util.List;
22+
import java.util.function.LongSupplier;
23+
24+
import org.springframework.data.domain.Pageable;
25+
import org.springframework.data.domain.Slice;
26+
import org.springframework.data.domain.SliceImpl;
2027
import org.springframework.data.domain.Sort;
2128
import org.springframework.data.jdbc.core.convert.JdbcConverter;
2229
import org.springframework.data.relational.core.dialect.Dialect;
@@ -26,9 +33,11 @@
2633
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
2734
import org.springframework.data.repository.query.Parameters;
2835
import org.springframework.data.repository.query.parser.PartTree;
36+
import org.springframework.data.support.PageableExecutionUtils;
2937
import org.springframework.jdbc.core.ResultSetExtractor;
3038
import org.springframework.jdbc.core.RowMapper;
3139
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
40+
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
3241
import org.springframework.util.Assert;
3342

3443
/**
@@ -46,6 +55,7 @@ public class PartTreeJdbcQuery extends AbstractJdbcQuery {
4655
private final JdbcConverter converter;
4756
private final PartTree tree;
4857
private final JdbcQueryExecution<?> execution;
58+
private final RowMapper<Object> rowMapper;
4959

5060
/**
5161
* Creates a new {@link PartTreeJdbcQuery}.
@@ -77,7 +87,9 @@ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod query
7787

7888
ResultSetExtractor<Boolean> extractor = tree.isExistsProjection() ? (ResultSet::next) : null;
7989

80-
this.execution = getQueryExecution(queryMethod, extractor, rowMapper);
90+
this.execution = queryMethod.isPageQuery() || queryMethod.isSliceQuery() ? collectionQuery(rowMapper)
91+
: getQueryExecution(queryMethod, extractor, rowMapper);
92+
this.rowMapper = rowMapper;
8193
}
8294

8395
private Sort getDynamicSort(RelationalParameterAccessor accessor) {
@@ -93,15 +105,108 @@ public Object execute(Object[] values) {
93105

94106
RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
95107
values);
96-
97108
ParametrizedQuery query = createQuery(accessor);
98-
return this.execution.execute(query.getQuery(), query.getParameterSource());
109+
JdbcQueryExecution<?> execution = getQueryExecution(accessor);
110+
111+
return execution.execute(query.getQuery(), query.getParameterSource());
112+
}
113+
114+
private JdbcQueryExecution<?> getQueryExecution(RelationalParametersParameterAccessor accessor) {
115+
116+
if (getQueryMethod().isSliceQuery()) {
117+
return new SliceQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.execution, accessor.getPageable());
118+
}
119+
120+
if (getQueryMethod().isPageQuery()) {
121+
122+
return new PageQueryExecution<>((JdbcQueryExecution<Collection<Object>>) this.execution, accessor.getPageable(),
123+
() -> {
124+
125+
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
126+
127+
JdbcCountQueryCreator queryCreator = new JdbcCountQueryCreator(context, tree, converter, dialect,
128+
entityMetadata, accessor, false);
129+
130+
ParametrizedQuery countQuery = queryCreator.createQuery(Sort.unsorted());
131+
Object count = singleObjectQuery((rs, i) -> rs.getLong(1)).execute(countQuery.getQuery(),
132+
countQuery.getParameterSource());
133+
134+
return converter.getConversionService().convert(count, Long.class);
135+
});
136+
}
137+
138+
return this.execution;
99139
}
100140

101141
protected ParametrizedQuery createQuery(RelationalParametersParameterAccessor accessor) {
102142

103143
RelationalEntityMetadata<?> entityMetadata = getQueryMethod().getEntityInformation();
104-
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor);
144+
145+
JdbcQueryCreator queryCreator = new JdbcQueryCreator(context, tree, converter, dialect, entityMetadata, accessor,
146+
getQueryMethod().isSliceQuery());
105147
return queryCreator.createQuery(getDynamicSort(accessor));
106148
}
149+
150+
/**
151+
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Slice}.
152+
*
153+
* @param <T>
154+
*/
155+
static class SliceQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
156+
157+
private final JdbcQueryExecution<? extends Collection<T>> delegate;
158+
private final Pageable pageable;
159+
160+
public SliceQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable) {
161+
this.delegate = delegate;
162+
this.pageable = pageable;
163+
}
164+
165+
@Override
166+
public Slice<T> execute(String query, SqlParameterSource parameter) {
167+
168+
Collection<T> result = delegate.execute(query, parameter);
169+
170+
int pageSize = 0;
171+
if (pageable.isPaged()) {
172+
173+
pageSize = pageable.getPageSize();
174+
}
175+
176+
List<T> resultList = result instanceof List ? (List<T>) result : new ArrayList<>(result);
177+
178+
boolean hasNext = pageable.isPaged() && resultList.size() > pageSize;
179+
180+
return new SliceImpl<>(hasNext ? resultList.subList(0, pageSize) : resultList, pageable, hasNext);
181+
}
182+
}
183+
184+
/**
185+
* {@link JdbcQueryExecution} returning a {@link org.springframework.data.domain.Page}.
186+
*
187+
* @param <T>
188+
*/
189+
static class PageQueryExecution<T> implements JdbcQueryExecution<Slice<T>> {
190+
191+
private final JdbcQueryExecution<? extends Collection<T>> delegate;
192+
private final Pageable pageable;
193+
private final LongSupplier countSupplier;
194+
195+
public PageQueryExecution(JdbcQueryExecution<? extends Collection<T>> delegate, Pageable pageable,
196+
LongSupplier countSupplier) {
197+
this.delegate = delegate;
198+
this.pageable = pageable;
199+
this.countSupplier = countSupplier;
200+
}
201+
202+
@Override
203+
public Slice<T> execute(String query, SqlParameterSource parameter) {
204+
205+
Collection<T> result = delegate.execute(query, parameter);
206+
207+
return PageableExecutionUtils.getPage(result instanceof List ? (List<T>) result : new ArrayList<>(result),
208+
pageable, countSupplier);
209+
}
210+
211+
}
107212
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
7373
this.queryMethod = queryMethod;
7474
this.converter = converter;
7575

76+
if (queryMethod.isSliceQuery()) {
77+
throw new UnsupportedOperationException(
78+
"Slice queries are not supported using string-based queries. Offending method: " + queryMethod);
79+
}
80+
81+
if (queryMethod.isPageQuery()) {
82+
throw new UnsupportedOperationException(
83+
"Page queries are not supported using string-based queries. Offending method: " + queryMethod);
84+
}
85+
7686
executor = Lazy.of(() -> {
7787
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
7888
return getQueryExecution( //

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@
2121
import org.junit.jupiter.api.extension.ExtendWith;
2222
import org.springframework.beans.factory.annotation.Autowired;
2323
import org.springframework.context.annotation.Bean;
24+
import org.springframework.context.annotation.ComponentScan;
2425
import org.springframework.context.annotation.Configuration;
26+
import org.springframework.context.annotation.FilterType;
2527
import org.springframework.context.annotation.Import;
2628
import org.springframework.data.annotation.Id;
2729
import org.springframework.data.jdbc.core.mapping.AggregateReference;
@@ -55,7 +57,8 @@ public class JdbcRepositoryCrossAggregateHsqlIntegrationTests {
5557

5658
@Configuration
5759
@Import(TestConfiguration.class)
58-
@EnableJdbcRepositories(considerNestedRepositories = true)
60+
@EnableJdbcRepositories(considerNestedRepositories = true,
61+
includeFilters = @ComponentScan.Filter(value = Ones.class, type = FilterType.ASSIGNABLE_TYPE))
5962
static class Config {
6063

6164
@Autowired JdbcRepositoryFactory factory;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,12 @@
2626

2727
import org.junit.jupiter.api.Test;
2828
import org.junit.jupiter.api.extension.ExtendWith;
29+
2930
import org.springframework.beans.factory.annotation.Autowired;
3031
import org.springframework.context.annotation.Bean;
3132
import org.springframework.context.annotation.ComponentScan;
3233
import org.springframework.context.annotation.Configuration;
34+
import org.springframework.context.annotation.FilterType;
3335
import org.springframework.context.annotation.Import;
3436
import org.springframework.data.annotation.Id;
3537
import org.springframework.data.jdbc.repository.config.EnableJdbcRepositories;
@@ -141,7 +143,8 @@ static class ImmutableWithManualIdEntity {
141143

142144
@Configuration
143145
@ComponentScan("org.springframework.data.jdbc.testing")
144-
@EnableJdbcRepositories(considerNestedRepositories = true)
146+
@EnableJdbcRepositories(considerNestedRepositories = true,
147+
includeFilters = @ComponentScan.Filter(value = CrudRepository.class, type = FilterType.ASSIGNABLE_TYPE))
145148
static class TestConfiguration {
146149

147150
AtomicLong lastId = new AtomicLong(0);

0 commit comments

Comments
 (0)