Add support for fluent limit(int) and scroll(OffsetScrollPosition) to Query by Example queries.

Closes #1609
This commit is contained in:
Mark Paluch
2023-09-14 08:40:56 +02:00
parent 6d1d96681f
commit bd1c670d55
8 changed files with 387 additions and 70 deletions

View File

@@ -33,12 +33,14 @@ abstract class ReactiveFluentQuerySupport<P, T> implements FluentQuery.ReactiveF
private final P predicate;
private final Sort sort;
private final int limit;
private final Class<T> resultType;
private final List<String> fieldsToInclude;
ReactiveFluentQuerySupport(P predicate, Sort sort, Class<T> resultType, List<String> fieldsToInclude) {
ReactiveFluentQuerySupport(P predicate, Sort sort, int limit, Class<T> resultType, List<String> fieldsToInclude) {
this.predicate = predicate;
this.sort = sort;
this.limit = limit;
this.resultType = resultType;
this.fieldsToInclude = fieldsToInclude;
}
@@ -48,7 +50,15 @@ abstract class ReactiveFluentQuerySupport<P, T> implements FluentQuery.ReactiveF
Assert.notNull(sort, "Sort must not be null");
return create(predicate, sort, resultType, fieldsToInclude);
return create(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
public ReactiveFluentQuery<T> limit(int limit) {
Assert.isTrue(limit >= 0, "Limit must not be negative");
return create(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
@@ -56,7 +66,7 @@ abstract class ReactiveFluentQuerySupport<P, T> implements FluentQuery.ReactiveF
Assert.notNull(projection, "Projection target type must not be null");
return create(predicate, sort, projection, fieldsToInclude);
return create(predicate, sort, limit, projection, fieldsToInclude);
}
@Override
@@ -64,10 +74,10 @@ abstract class ReactiveFluentQuerySupport<P, T> implements FluentQuery.ReactiveF
Assert.notNull(properties, "Projection properties must not be null");
return create(predicate, sort, resultType, new ArrayList<>(properties));
return create(predicate, sort, limit, resultType, new ArrayList<>(properties));
}
protected abstract <R> ReactiveFluentQuerySupport<P, R> create(P predicate, Sort sort, Class<R> resultType,
protected abstract <R> ReactiveFluentQuerySupport<P, R> create(P predicate, Sort sort, int limit, Class<R> resultType,
List<String> fieldsToInclude);
P getPredicate() {
@@ -78,6 +88,10 @@ abstract class ReactiveFluentQuerySupport<P, T> implements FluentQuery.ReactiveF
return sort;
}
int getLimit() {
return limit;
}
Class<T> getResultType() {
return resultType;
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright 2023 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.r2dbc.repository.support;
import java.util.List;
import java.util.function.IntFunction;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Window;
/**
* Delegate to handle {@link ScrollPosition scroll queries} and create result {@link Window}.
*
* @author Mark Paluch
* @since 3.1.4
*/
public class ScrollDelegate {
static <T> Window<T> createWindow(List<T> result, int limit, IntFunction<? extends ScrollPosition> positionFunction) {
return Window.from(getFirst(limit, result), positionFunction, hasMoreElements(result, limit));
}
private static boolean hasMoreElements(List<?> result, int limit) {
return !result.isEmpty() && result.size() > limit;
}
/**
* Return the first {@code count} items from the list.
*
* @param count the number of first elements to be included in the returned list.
* @param list must not be {@literal null}
* @return the returned sublist if the {@code list} is greater {@code count}.
* @param <T> the element type of the lists.
*/
public static <T> List<T> getFirst(int count, List<T> list) {
if (count > 0 && list.size() > count) {
return list.subList(0, count);
}
return list;
}
}

View File

@@ -25,9 +25,12 @@ import java.util.function.UnaryOperator;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.domain.Window;
import org.springframework.data.r2dbc.convert.R2dbcConverter;
import org.springframework.data.r2dbc.core.R2dbcEntityOperations;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
@@ -367,17 +370,18 @@ public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {
class ReactiveFluentQueryByExample<S, T> extends ReactiveFluentQuerySupport<Example<S>, T> {
ReactiveFluentQueryByExample(Example<S> example, Class<T> resultType) {
this(example, Sort.unsorted(), resultType, Collections.emptyList());
this(example, Sort.unsorted(), 0, resultType, Collections.emptyList());
}
ReactiveFluentQueryByExample(Example<S> example, Sort sort, Class<T> resultType, List<String> fieldsToInclude) {
super(example, sort, resultType, fieldsToInclude);
ReactiveFluentQueryByExample(Example<S> example, Sort sort, int limit, Class<T> resultType,
List<String> fieldsToInclude) {
super(example, sort, limit, resultType, fieldsToInclude);
}
@Override
protected <R> ReactiveFluentQueryByExample<S, R> create(Example<S> predicate, Sort sort, Class<R> resultType,
List<String> fieldsToInclude) {
return new ReactiveFluentQueryByExample<>(predicate, sort, resultType, fieldsToInclude);
protected <R> ReactiveFluentQueryByExample<S, R> create(Example<S> predicate, Sort sort, int limit,
Class<R> resultType, List<String> fieldsToInclude) {
return new ReactiveFluentQueryByExample<>(predicate, sort, limit, resultType, fieldsToInclude);
}
@Override
@@ -395,6 +399,34 @@ public class SimpleR2dbcRepository<T, ID> implements R2dbcRepository<T, ID> {
return createQuery().all();
}
@Override
public Mono<Window<T>> scroll(ScrollPosition scrollPosition) {
Assert.notNull(scrollPosition, "ScrollPosition must not be null");
if (scrollPosition instanceof OffsetScrollPosition osp) {
int limit = getLimit();
return createQuery(q -> {
Query queryToUse = q.offset(osp.getOffset());
if (limit > 0) {
queryToUse = queryToUse.limit(limit + 1);
}
return queryToUse;
}).all() //
.collectList() //
.map(content -> {
return ScrollDelegate.createWindow(content, limit,
OffsetScrollPosition.positionFunction(osp.getOffset()));
});
}
return super.scroll(scrollPosition);
}
@Override
public Mono<Page<T>> page(Pageable pageable) {

View File

@@ -15,6 +15,23 @@
*/
package org.springframework.data.r2dbc.repository.support;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.ExampleMatcher.*;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;
import static org.springframework.data.domain.ExampleMatcher.StringMatcher.*;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import javax.sql.DataSource;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
@@ -25,6 +42,7 @@ import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.convert.MappingR2dbcConverter;
import org.springframework.data.r2dbc.core.ReactiveDataAccessStrategy;
@@ -37,21 +55,6 @@ import org.springframework.data.relational.repository.support.MappingRelationalE
import org.springframework.data.repository.query.FluentQuery;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.r2dbc.core.DatabaseClient;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import javax.sql.DataSource;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.ExampleMatcher.GenericPropertyMatchers.*;
import static org.springframework.data.domain.ExampleMatcher.StringMatcher.*;
import static org.springframework.data.domain.ExampleMatcher.*;
/**
* Abstract integration tests for {@link SimpleR2dbcRepository} to be ran against various databases.
@@ -832,6 +835,36 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
.verifyComplete();
}
@Test // GH-1609
void findByScrollPosition() {
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('FORSCHUNGSSCHIFF', 13)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('SCHAUFELRADBAGGER', 13)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('VOLTRON', 13)");
jdbc.execute("INSERT INTO legoset (name, manual) VALUES('RALLYEAUTO', 14)");
LegoSet probe = new LegoSet();
probe.setManual(13);
repository
.findBy(Example.of(probe, matching().withIgnorePaths("id")),
q -> q.sortBy(Sort.by("name")).limit(2).scroll(ScrollPosition.offset())) //
.as(StepVerifier::create) //
.consumeNextWith(window -> {
assertThat(window.map(it -> it.name)).containsOnly("FORSCHUNGSSCHIFF", "SCHAUFELRADBAGGER");
}).verifyComplete();
repository
.findBy(Example.of(probe, matching().withIgnorePaths("id")),
q -> q.sortBy(Sort.by("name")).limit(2).scroll(ScrollPosition.offset(2))) //
.as(StepVerifier::create) //
.consumeNextWith(window -> {
assertThat(window.map(it -> it.name)).containsOnly("VOLTRON");
}).verifyComplete();
}
@Test // GH-663
void findByShouldApplySortAll() {
@@ -981,8 +1014,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Table("legoset")
static class LegoSet {
@Id
int id;
@Id int id;
String name;
Integer manual;
@@ -992,8 +1024,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
this.manual = manual;
}
public LegoSet() {
}
public LegoSet() {}
public int getId() {
return this.id;
@@ -1027,8 +1058,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Table("legoset")
static class LegoSetWithNonScalarId {
@Id
Integer id;
@Id Integer id;
String name;
Integer manual;
String extra;
@@ -1040,8 +1070,7 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
this.extra = extra;
}
public LegoSetWithNonScalarId() {
}
public LegoSetWithNonScalarId() {}
public Integer getId() {
return this.id;
@@ -1077,10 +1106,13 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
LegoSetWithNonScalarId that = (LegoSetWithNonScalarId) o;
return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(manual, that.manual) && Objects.equals(extra, that.extra);
return Objects.equals(id, that.id) && Objects.equals(name, that.name) && Objects.equals(manual, that.manual)
&& Objects.equals(extra, that.extra);
}
@Override
@@ -1092,16 +1124,14 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Table("legoset")
static class LegoSetVersionable extends LegoSet {
@Version
Integer version;
@Version Integer version;
LegoSetVersionable(int id, String name, Integer manual, Integer version) {
super(id, name, manual);
this.version = version;
}
public LegoSetVersionable() {
}
public LegoSetVersionable() {}
public Integer getVersion() {
return this.version;
@@ -1115,16 +1145,14 @@ public abstract class AbstractSimpleR2dbcRepositoryIntegrationTests extends R2db
@Table("legoset")
static class LegoSetPrimitiveVersionable extends LegoSet {
@Version
int version;
@Version int version;
LegoSetPrimitiveVersionable(int id, String name, Integer manual, int version) {
super(id, name, manual);
this.version = version;
}
public LegoSetPrimitiveVersionable() {
}
public LegoSetPrimitiveVersionable() {}
public int getVersion() {
return this.version;