Skip to content

Add support for Slice and Page queries using query derivation #952

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>2.2.0-SNAPSHOT</version>
<version>2.2.0-GH-774-SNAPSHOT</version>
<packaging>pom</packaging>

<name>Spring Data Relational Parent</name>
Expand Down
2 changes: 1 addition & 1 deletion spring-data-jdbc-distribution/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>2.2.0-SNAPSHOT</version>
<version>2.2.0-GH-774-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>

Expand Down
4 changes: 2 additions & 2 deletions spring-data-jdbc/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-data-jdbc</artifactId>
<version>2.2.0-SNAPSHOT</version>
<version>2.2.0-GH-774-SNAPSHOT</version>

<name>Spring Data JDBC</name>
<description>Spring Data module for JDBC repositories.</description>
Expand All @@ -15,7 +15,7 @@
<parent>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational-parent</artifactId>
<version>2.2.0-SNAPSHOT</version>
<version>2.2.0-GH-774-SNAPSHOT</version>
</parent>

<properties>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ private JdbcQueryExecution<Object> createModifyingQueryExecutor() {
};
}

private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {

return (query, parameters) -> {
try {
Expand All @@ -119,7 +119,7 @@ private JdbcQueryExecution<Object> singleObjectQuery(RowMapper<?> rowMapper) {
};
}

private <T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
<T> JdbcQueryExecution<List<T>> collectionQuery(RowMapper<T> rowMapper) {
return getQueryExecution(new RowMapperResultSetExtractor<>(rowMapper));
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand All @@ -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");
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -171,23 +174,23 @@ protected ParametrizedQuery complete(@Nullable Criteria criteria, Sort sort) {
return new ParametrizedQuery(sql, parameterSource);
}

private SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
SelectBuilder.SelectOrdered applyOrderBy(Sort sort, RelationalPersistentEntity<?> entity, Table table,
SelectBuilder.SelectOrdered selectOrdered) {

return sort.isSorted() ? //
selectOrdered.orderBy(queryMapper.getMappedSort(table, sort, entity)) //
: 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 //
? whereBuilder.where(queryMapper.getMappedObject(parameterSource, criteria, table, entity)) //
: whereBuilder;
}

private SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {
SelectBuilder.SelectWhere applyLimitAndOffset(SelectBuilder.SelectLimitOffset limitOffsetBuilder) {

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

Pageable pageable = accessor.getPageable();
if (pageable.isPaged()) {
limitOffsetBuilder = limitOffsetBuilder.limit(pageable.getPageSize()).offset(pageable.getOffset());
limitOffsetBuilder = limitOffsetBuilder.limit(isSliceQuery ? pageable.getPageSize() + 1 : pageable.getPageSize())
.offset(pageable.getOffset());
}

return (SelectBuilder.SelectWhere) limitOffsetBuilder;
}

private SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {
SelectBuilder.SelectLimitOffset createSelectClause(RelationalPersistentEntity<?> entity, Table table) {

SelectBuilder.SelectJoin builder;
if (tree.isExistsProjection()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,9 +33,11 @@
import org.springframework.data.relational.repository.query.RelationalParametersParameterAccessor;
import org.springframework.data.repository.query.Parameters;
import org.springframework.data.repository.query.parser.PartTree;
import org.springframework.data.support.PageableExecutionUtils;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcOperations;
import org.springframework.jdbc.core.namedparam.SqlParameterSource;
import org.springframework.util.Assert;

/**
Expand All @@ -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}.
Expand Down Expand Up @@ -77,7 +87,9 @@ public PartTreeJdbcQuery(RelationalMappingContext context, JdbcQueryMethod query

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) {
Expand All @@ -93,15 +105,108 @@ public Object execute(Object[] values) {

RelationalParametersParameterAccessor accessor = new RelationalParametersParameterAccessor(getQueryMethod(),
values);

ParametrizedQuery query = createQuery(accessor);
return this.execution.execute(query.getQuery(), query.getParameterSource());
JdbcQueryExecution<?> execution = getQueryExecution(accessor);

return execution.execute(query.getQuery(), query.getParameterSource());
}

private JdbcQueryExecution<?> getQueryExecution(RelationalParametersParameterAccessor accessor) {

if (getQueryMethod().isSliceQuery()) {
return new SliceQueryExecution<>((JdbcQueryExecution<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);
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,16 @@ public StringBasedJdbcQuery(JdbcQueryMethod queryMethod, NamedParameterJdbcOpera
this.queryMethod = queryMethod;
this.converter = converter;

if (queryMethod.isSliceQuery()) {
throw new UnsupportedOperationException(
"Slice queries are not supported using string-based queries. Offending method: " + queryMethod);
}

if (queryMethod.isPageQuery()) {
throw new UnsupportedOperationException(
"Page queries are not supported using string-based queries. Offending method: " + queryMethod);
}

executor = Lazy.of(() -> {
RowMapper<Object> rowMapper = determineRowMapper(defaultRowMapper);
return getQueryExecution( //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.Import;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
Expand Down Expand Up @@ -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;
Expand Down
Loading