Revise default ScrollSubrange for QueryDsl and QBE

The QueryDsl and QBE data fetcher builders now accept a default count
and a function for a default position with awareness of the scroll
direction.

Closes gh-917
This commit is contained in:
rstoyanchev
2024-02-20 22:52:08 +00:00
parent 9817230638
commit 248cfe22ba
4 changed files with 264 additions and 117 deletions

View File

@@ -35,6 +35,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.Example;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
@@ -370,24 +371,29 @@ public abstract class QueryByExampleDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
@Nullable
private final ScrollSubrange defaultSubrange;
private final Integer defaultScrollCount;
@Nullable
private final Function<Boolean, ScrollPosition> defaultScrollPosition;
private final Sort sort;
@SuppressWarnings("unchecked")
Builder(QueryByExampleExecutor<T> executor, Class<R> domainType) {
this(executor, TypeInformation.of((Class<T>) domainType), domainType, null, null, Sort.unsorted());
this(executor, TypeInformation.of((Class<T>) domainType), domainType, null, null, null, Sort.unsorted());
}
Builder(QueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy, @Nullable ScrollSubrange defaultSubrange,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy,
@Nullable Integer defaultScrollCount, @Nullable Function<Boolean, ScrollPosition> defaultScrollPosition,
Sort sort) {
this.executor = executor;
this.domainType = domainType;
this.resultType = resultType;
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultScrollCount = defaultScrollCount;
this.defaultScrollPosition = defaultScrollPosition;
this.sort = sort;
}
@@ -402,8 +408,8 @@ public abstract class QueryByExampleDataFetcher<T> {
*/
public <P> Builder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
return new Builder<>(this.executor, this.domainType,
projectionType, this.cursorStrategy, this.defaultSubrange, this.sort);
return new Builder<>(this.executor, this.domainType, projectionType,
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort);
}
/**
@@ -416,8 +422,26 @@ public abstract class QueryByExampleDataFetcher<T> {
* @since 1.2.0
*/
public Builder<T, R> cursorStrategy(@Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
return new Builder<>(this.executor, this.domainType, this.resultType,
cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort);
}
/**
* Configure a default scroll count to use, and function to return a default
* {@link ScrollPosition} for forward vs backward pagination.
* <p>For offset scrolling, use {@link ScrollPosition#offset()} to scroll
* from the beginning. Currently, it is not possible to go back from the end.
* <p>For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll
* from the beginning, or {@link KeysetScrollPosition#reverse()} the same
* to go back from the end.
* <p>By default a count of 20 and {@link ScrollPosition#offset()} are used.
* @since 1.2.5
*/
public Builder<T, R> defaultScrollSubrange(
int defaultCount, Function<Boolean, ScrollPosition> defaultPosition) {
return new Builder<>(this.executor, this.domainType,
this.resultType, cursorStrategy, this.defaultSubrange, this.sort);
this.resultType, this.cursorStrategy, defaultCount, defaultPosition, this.sort);
}
/**
@@ -427,11 +451,16 @@ public abstract class QueryByExampleDataFetcher<T> {
* count of 20.
* @return a new {@link Builder} instance with all previously configured
* options and {@code Sort} applied
* @since 1.2.0
* @deprecated in favor of {@link #defaultScrollSubrange(int, Function)}
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Deprecated(since = "1.2.5", forRemoval = true)
public Builder<T, R> defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) {
return new Builder<>(this.executor, this.domainType,
this.resultType, this.cursorStrategy, defaultSubrange, this.sort);
this.resultType, this.cursorStrategy,
(defaultSubrange != null ? defaultSubrange.count().getAsInt() : null),
(defaultSubrange != null ? forward -> defaultSubrange.position().get() : null),
this.sort);
}
/**
@@ -442,8 +471,8 @@ public abstract class QueryByExampleDataFetcher<T> {
*/
public Builder<T, R> sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
return new Builder<>(this.executor, this.domainType,
this.resultType, this.cursorStrategy, this.defaultSubrange, sort);
return new Builder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, sort);
}
/**
@@ -469,7 +498,8 @@ public abstract class QueryByExampleDataFetcher<T> {
return new ScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
(this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()),
(this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()),
(this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()),
(this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()),
this.sort);
}
@@ -516,25 +546,30 @@ public abstract class QueryByExampleDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
@Nullable
private final ScrollSubrange defaultSubrange;
private final Integer defaultScrollCount;
@Nullable
private final Function<Boolean, ScrollPosition> defaultScrollPosition;
private final Sort sort;
@SuppressWarnings("unchecked")
ReactiveBuilder(ReactiveQueryByExampleExecutor<T> executor, Class<R> domainType) {
this(executor, TypeInformation.of((Class<T>) domainType), domainType, null, null, Sort.unsorted());
this(executor, TypeInformation.of((Class<T>) domainType), domainType, null, null, null, Sort.unsorted());
}
ReactiveBuilder(
ReactiveQueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy, @Nullable ScrollSubrange defaultSubrange,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy,
@Nullable Integer defaultScrollCount, @Nullable Function<Boolean, ScrollPosition> defaultScrollPosition,
Sort sort) {
this.executor = executor;
this.domainType = domainType;
this.resultType = resultType;
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultScrollCount = defaultScrollCount;
this.defaultScrollPosition = defaultScrollPosition;
this.sort = sort;
}
@@ -550,7 +585,7 @@ public abstract class QueryByExampleDataFetcher<T> {
public <P> ReactiveBuilder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
return new ReactiveBuilder<>(this.executor, this.domainType,
projectionType, this.cursorStrategy, this.defaultSubrange, this.sort);
projectionType, this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort);
}
/**
@@ -563,8 +598,26 @@ public abstract class QueryByExampleDataFetcher<T> {
* @since 1.2.0
*/
public ReactiveBuilder<T, R> cursorStrategy(@Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, this.sort);
}
/**
* Configure a default scroll count to use, and function to return a default
* {@link ScrollPosition} for forward vs backward pagination.
* <p>For offset scrolling, use {@link ScrollPosition#offset()} to scroll
* from the beginning. Currently, it is not possible to go back from the end.
* <p>For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll
* from the beginning, or {@link KeysetScrollPosition#reverse()} the same
* to go back from the end.
* <p>By default a count of 20 and {@link ScrollPosition#offset()} are used.
* @since 1.2.5
*/
public ReactiveBuilder<T, R> defaultScrollSubrange(
int defaultCount, Function<Boolean, ScrollPosition> defaultPosition) {
return new ReactiveBuilder<>(this.executor, this.domainType,
this.resultType, cursorStrategy, this.defaultSubrange, this.sort);
this.resultType, this.cursorStrategy, defaultCount, defaultPosition, this.sort);
}
/**
@@ -574,11 +627,16 @@ public abstract class QueryByExampleDataFetcher<T> {
* count of 20.
* @return a new {@link Builder} instance with all previously configured
* options and {@code Sort} applied
* @since 1.2.0
* @deprecated in favor of {@link #defaultScrollSubrange(int, Function)}
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Deprecated(since = "1.2.5", forRemoval = true)
public ReactiveBuilder<T, R> defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) {
return new ReactiveBuilder<>(this.executor, this.domainType,
this.resultType, this.cursorStrategy, defaultSubrange, this.sort);
this.resultType, this.cursorStrategy,
(defaultSubrange != null ? defaultSubrange.count().getAsInt() : null),
(defaultSubrange != null ? forward -> defaultSubrange.position().get() : null),
this.sort);
}
/**
@@ -589,8 +647,8 @@ public abstract class QueryByExampleDataFetcher<T> {
*/
public ReactiveBuilder<T, R> sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
return new ReactiveBuilder<>(this.executor, this.domainType,
this.resultType, this.cursorStrategy, this.defaultSubrange, sort);
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition, sort);
}
/**
@@ -616,7 +674,8 @@ public abstract class QueryByExampleDataFetcher<T> {
return new ReactiveScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
(this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()),
(this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()),
(this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()),
(this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()),
this.sort);
}
@@ -747,23 +806,27 @@ public abstract class QueryByExampleDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
private final ScrollSubrange defaultSubrange;
private final int defaultCount;
private final Function<Boolean, ScrollPosition> defaultPosition;
private final ResolvableType scrollableResultType;
ScrollableEntityFetcher(
QueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) {
CursorStrategy<ScrollPosition> cursorStrategy,
int defaultCount,
Function<Boolean, ScrollPosition> defaultPosition,
Sort sort) {
super(executor, domainType, resultType, sort);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
Assert.notNull(defaultPosition, "'defaultPosition' is required");
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultCount = defaultCount;
this.defaultPosition = defaultPosition;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Window.class, resultType);
}
@@ -772,12 +835,12 @@ public abstract class QueryByExampleDataFetcher<T> {
return ResolvableType.forClassWithGenerics(Iterable.class, this.scrollableResultType);
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
protected Iterable<R> getResult(FluentQuery.FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange);
int count = range.count().getAsInt();
ScrollPosition position = range.position().get();
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
int count = range.count().orElse(this.defaultCount);
ScrollPosition position = (range.position().isPresent() ?
range.position().get() : this.defaultPosition.apply(range.forward()));
return queryToUse.limit(count).scroll(position);
}
@@ -891,26 +954,30 @@ public abstract class QueryByExampleDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
private final ScrollSubrange defaultSubrange;
private final int defaultCount;
private final Function<Boolean, ScrollPosition> defaultPosition;
private final Sort sort;
ReactiveScrollableEntityFetcher(
ReactiveQueryByExampleExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultSubrange, Sort sort) {
CursorStrategy<ScrollPosition> cursorStrategy,
int defaultCount,
Function<Boolean, ScrollPosition> defaultPosition,
Sort sort) {
super(domainType);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
Assert.notNull(defaultPosition, "'defaultPosition' is required");
this.executor = executor;
this.resultType = resultType;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType);
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultCount = defaultCount;
this.defaultPosition = defaultPosition;
this.sort = sort;
}
@@ -920,7 +987,7 @@ public abstract class QueryByExampleDataFetcher<T> {
}
@Override
@SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"})
@SuppressWarnings("unchecked")
public Mono<Iterable<R>> get(DataFetchingEnvironment env) throws BindException {
return this.executor.findBy(buildExample(env), query -> {
FluentQuery.ReactiveFluentQuery<R> queryToUse = (FluentQuery.ReactiveFluentQuery<R>) query;
@@ -936,9 +1003,10 @@ public abstract class QueryByExampleDataFetcher<T> {
queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
}
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange);
int count = range.count().getAsInt();
ScrollPosition position = range.position().get();
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
int count = range.count().orElse(this.defaultCount);
ScrollPosition position = (range.position().isPresent() ?
range.position().get() : this.defaultPosition.apply(range.forward()));
return queryToUse.limit(count).scroll(position).map(Function.identity());
});
}

View File

@@ -36,6 +36,7 @@ import reactor.core.publisher.Mono;
import org.springframework.core.ResolvableType;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.KeysetScrollPosition;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
@@ -412,7 +413,10 @@ public abstract class QuerydslDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
@Nullable
private final ScrollSubrange defaultSubrange;
private final Integer defaultScrollCount;
@Nullable
private final Function<Boolean, ScrollPosition> defaultScrollPosition;
private final Sort sort;
@@ -421,18 +425,20 @@ public abstract class QuerydslDataFetcher<T> {
@SuppressWarnings("unchecked")
Builder(QuerydslPredicateExecutor<T> executor, Class<R> domainType) {
this(executor, TypeInformation.of((Class<T>) domainType),
domainType, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER);
domainType, null, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER);
}
Builder(QuerydslPredicateExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy, @Nullable ScrollSubrange defaultSubrange,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy,
@Nullable Integer defaultScrollCount, @Nullable Function<Boolean, ScrollPosition> defaultScrollPosition,
Sort sort, QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
this.executor = executor;
this.domainType = domainType;
this.resultType = resultType;
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultScrollCount = defaultScrollCount;
this.defaultScrollPosition = defaultScrollPosition;
this.sort = sort;
this.customizer = customizer;
}
@@ -449,7 +455,8 @@ public abstract class QuerydslDataFetcher<T> {
public <P> Builder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
return new Builder<>(this.executor, this.domainType, projectionType,
this.cursorStrategy, this.defaultSubrange, this.sort, this.customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, this.customizer);
}
/**
@@ -463,21 +470,43 @@ public abstract class QuerydslDataFetcher<T> {
*/
public Builder<T, R> cursorStrategy(@Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
return new Builder<>(this.executor, this.domainType, this.resultType,
cursorStrategy, this.defaultSubrange, this.sort, this.customizer);
cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, this.customizer);
}
/**
* Configure a default scroll count to use, and function to return a default
* {@link ScrollPosition} for forward vs backward pagination.
* <p>For offset scrolling, use {@link ScrollPosition#offset()} to scroll
* from the beginning. Currently, it is not possible to go back from the end.
* <p>For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll
* from the beginning, or {@link KeysetScrollPosition#reverse()} the same
* to go back from the end.
* <p>By default a count of 20 and {@link ScrollPosition#offset()} are used.
* @since 1.2.5
*/
public Builder<T, R> defaultScrollSubrange(
int defaultCount, Function<Boolean, ScrollPosition> defaultPosition) {
return new Builder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, defaultCount, defaultPosition, this.sort, this.customizer);
}
/**
* Configure a {@link ScrollSubrange} to use when a paginated request does
* not specify a cursor and/or a count of items.
* <p>By default, this is {@link OffsetScrollPosition#offset()} with a
* count of 20.
* @return a new {@link Builder} instance with all previously configured
* options and {@code Sort} applied
* <p>By default, this is {@link OffsetScrollPosition#offset()} with a count of 20.
* @return a new {@link Builder} instance
* @since 1.2.0
* @deprecated in favor of {@link #defaultScrollSubrange(int, Function)}
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Deprecated(since = "1.2.5", forRemoval = true)
public Builder<T, R> defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) {
return new Builder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, defaultSubrange, this.sort, this.customizer);
return new Builder<>(this.executor, this.domainType, this.resultType, this.cursorStrategy,
(defaultSubrange != null ? defaultSubrange.count().getAsInt() : null),
(defaultSubrange != null ? forward -> defaultSubrange.position().get() : null),
this.sort, this.customizer);
}
/**
@@ -489,7 +518,8 @@ public abstract class QuerydslDataFetcher<T> {
public Builder<T, R> sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
return new Builder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultSubrange, sort, customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
sort, customizer);
}
/**
@@ -508,7 +538,8 @@ public abstract class QuerydslDataFetcher<T> {
public Builder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
return new Builder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultSubrange, this.sort, customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, customizer);
}
/**
@@ -536,7 +567,8 @@ public abstract class QuerydslDataFetcher<T> {
return new ScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
(this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()),
(this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()),
(this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()),
(this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()),
this.sort, this.customizer);
}
@@ -583,7 +615,10 @@ public abstract class QuerydslDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
@Nullable
private final ScrollSubrange defaultSubrange;
private final Integer defaultScrollCount;
@Nullable
private final Function<Boolean, ScrollPosition> defaultScrollPosition;
private final Sort sort;
@@ -592,19 +627,21 @@ public abstract class QuerydslDataFetcher<T> {
@SuppressWarnings("unchecked")
ReactiveBuilder(ReactiveQuerydslPredicateExecutor<T> executor, Class<R> domainType) {
this(executor, TypeInformation.of((Class<T>) domainType),
domainType, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER);
domainType, null, null, null, Sort.unsorted(), NO_OP_BINDER_CUSTOMIZER);
}
ReactiveBuilder(
ReactiveQuerydslPredicateExecutor<T> executor, TypeInformation<T> domainType, Class<R> resultType,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy, @Nullable ScrollSubrange defaultSubrange,
@Nullable CursorStrategy<ScrollPosition> cursorStrategy,
@Nullable Integer defaultScrollCount, @Nullable Function<Boolean, ScrollPosition> defaultScrollPosition,
Sort sort, QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
this.executor = executor;
this.domainType = domainType;
this.resultType = resultType;
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultScrollCount = defaultScrollCount;
this.defaultScrollPosition = defaultScrollPosition;
this.sort = sort;
this.customizer = customizer;
}
@@ -621,7 +658,8 @@ public abstract class QuerydslDataFetcher<T> {
public <P> ReactiveBuilder<T, P> projectAs(Class<P> projectionType) {
Assert.notNull(projectionType, "Projection type must not be null");
return new ReactiveBuilder<>(this.executor, this.domainType, projectionType,
this.cursorStrategy, this.defaultSubrange, this.sort, this.customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, this.customizer);
}
/**
@@ -635,21 +673,44 @@ public abstract class QuerydslDataFetcher<T> {
*/
public ReactiveBuilder<T, R> cursorStrategy(@Nullable CursorStrategy<ScrollPosition> cursorStrategy) {
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
cursorStrategy, this.defaultSubrange, this.sort, this.customizer);
cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, this.customizer);
}
/**
* Configure a default scroll count to use, and function to return a default
* {@link ScrollPosition} for forward vs backward pagination.
* <p>For offset scrolling, use {@link ScrollPosition#offset()} to scroll
* from the beginning. Currently, it is not possible to go back from the end.
* <p>For keyset scrolling, use {@link ScrollPosition#keyset()} to scroll
* from the beginning, or {@link KeysetScrollPosition#reverse()} the same
* to go back from the end.
* <p>By default a count of 20 and {@link ScrollPosition#offset()} are used.
* @since 1.2.5
*/
public ReactiveBuilder<T, R> defaultScrollSubrange(
int defaultCount, Function<Boolean, ScrollPosition> defaultPosition) {
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, defaultCount, defaultPosition, this.sort, this.customizer);
}
/**
* Configure a {@link ScrollSubrange} to use when a paginated request does
* not specify a cursor and/or a count of items.
* <p>By default, this is {@link OffsetScrollPosition#offset()} with a
* count of 20.
* @return a new {@link Builder} instance with all previously configured
* options and {@code Sort} applied
* <p>By default, this is {@link OffsetScrollPosition#offset()} with a count of 20.
* @return a new {@link Builder} instance
* @since 1.2.0
* @deprecated in favor of {@link #defaultScrollSubrange(int, Function)}
*/
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Deprecated(since = "1.2.5", forRemoval = true)
public ReactiveBuilder<T, R> defaultScrollSubrange(@Nullable ScrollSubrange defaultSubrange) {
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, defaultSubrange, this.sort, this.customizer);
this.cursorStrategy,
(defaultSubrange != null ? defaultSubrange.count().getAsInt() : null),
(defaultSubrange != null ? forward -> defaultSubrange.position().get() : null),
this.sort, this.customizer);
}
/**
@@ -661,7 +722,8 @@ public abstract class QuerydslDataFetcher<T> {
public ReactiveBuilder<T, R> sortBy(Sort sort) {
Assert.notNull(sort, "Sort must not be null");
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultSubrange, sort, this.customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
sort, this.customizer);
}
/**
@@ -680,7 +742,8 @@ public abstract class QuerydslDataFetcher<T> {
public ReactiveBuilder<T, R> customizer(QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
Assert.notNull(customizer, "QuerydslBinderCustomizer must not be null");
return new ReactiveBuilder<>(this.executor, this.domainType, this.resultType,
this.cursorStrategy, this.defaultSubrange, this.sort, customizer);
this.cursorStrategy, this.defaultScrollCount, this.defaultScrollPosition,
this.sort, customizer);
}
/**
@@ -708,7 +771,8 @@ public abstract class QuerydslDataFetcher<T> {
return new ReactiveScrollableEntityFetcher<>(
this.executor, this.domainType, this.resultType,
(this.cursorStrategy != null ? this.cursorStrategy : RepositoryUtils.defaultCursorStrategy()),
(this.defaultSubrange != null ? this.defaultSubrange : RepositoryUtils.defaultScrollSubrange()),
(this.defaultScrollCount != null ? this.defaultScrollCount : RepositoryUtils.defaultScrollCount()),
(this.defaultScrollPosition != null ? this.defaultScrollPosition : RepositoryUtils.defaultScrollPosition()),
this.sort, this.customizer);
}
@@ -843,33 +907,35 @@ public abstract class QuerydslDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
private final ScrollSubrange defaultSubrange;
private final int defaultCount;
private final Function<Boolean, ScrollPosition> defaultPosition;
ScrollableEntityFetcher(QuerydslPredicateExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy,
ScrollSubrange defaultSubrange,
int defaultCount,
Function<Boolean, ScrollPosition> defaultPosition,
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(executor, domainType, resultType, sort, customizer);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
Assert.notNull(defaultPosition, "'defaultPosition' is required");
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultCount = defaultCount;
this.defaultPosition = defaultPosition;
}
@SuppressWarnings("OptionalGetWithoutIsPresent")
@Override
protected Iterable<R> getResult(FetchableFluentQuery<R> queryToUse, DataFetchingEnvironment env) {
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange);
int count = range.count().getAsInt();
ScrollPosition position = range.position().get();
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
int count = range.count().orElse(this.defaultCount);
ScrollPosition position = (range.position().isPresent() ?
range.position().get() : this.defaultPosition.apply(range.forward()));
return queryToUse.limit(count).scroll(position);
}
@@ -987,7 +1053,9 @@ public abstract class QuerydslDataFetcher<T> {
private final CursorStrategy<ScrollPosition> cursorStrategy;
private final ScrollSubrange defaultSubrange;
private final int defaultCount;
private final Function<Boolean, ScrollPosition> defaultPosition;
private final Sort sort;
@@ -995,22 +1063,23 @@ public abstract class QuerydslDataFetcher<T> {
ReactiveScrollableEntityFetcher(ReactiveQuerydslPredicateExecutor<T> executor,
TypeInformation<T> domainType,
Class<R> resultType,
CursorStrategy<ScrollPosition> cursorStrategy, ScrollSubrange defaultSubrange,
CursorStrategy<ScrollPosition> cursorStrategy,
int defaultCount,
Function<Boolean, ScrollPosition> defaultPosition,
Sort sort,
QuerydslBinderCustomizer<? extends EntityPath<T>> customizer) {
super(domainType, (QuerydslBinderCustomizer) customizer);
Assert.notNull(cursorStrategy, "CursorStrategy is required");
Assert.notNull(defaultSubrange, "Default ScrollSubrange is required");
Assert.isTrue(defaultSubrange.position().isPresent(), "Default ScrollPosition is required");
Assert.isTrue(defaultSubrange.count().isPresent(), "Default scroll limit is required");
Assert.notNull(defaultPosition, "'defaultPosition' is required");
this.executor = executor;
this.resultType = resultType;
this.scrollableResultType = ResolvableType.forClassWithGenerics(Iterable.class, resultType);
this.cursorStrategy = cursorStrategy;
this.defaultSubrange = defaultSubrange;
this.defaultCount = defaultCount;
this.defaultPosition = defaultPosition;
this.sort = sort;
}
@@ -1020,7 +1089,7 @@ public abstract class QuerydslDataFetcher<T> {
}
@Override
@SuppressWarnings({"unchecked", "OptionalGetWithoutIsPresent"})
@SuppressWarnings("unchecked")
public Mono<Iterable<R>> get(DataFetchingEnvironment env) {
return this.executor.findBy(buildPredicate(env), query -> {
FluentQuery.ReactiveFluentQuery<R> queryToUse = (FluentQuery.ReactiveFluentQuery<R>) query;
@@ -1036,9 +1105,10 @@ public abstract class QuerydslDataFetcher<T> {
queryToUse = queryToUse.project(buildPropertyPaths(env.getSelectionSet(), this.resultType));
}
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy, this.defaultSubrange);
int count = range.count().getAsInt();
ScrollPosition position = range.position().get();
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, this.cursorStrategy);
int count = range.count().orElse(this.defaultCount);
ScrollPosition position = (range.position().isPresent() ?
range.position().get() : this.defaultPosition.apply(range.forward()));
return queryToUse.limit(count).scroll(position).map(Function.identity());
});
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2023 the original author or authors.
* Copyright 2002-2024 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.
@@ -16,6 +16,7 @@
package org.springframework.graphql.data.query;
import java.lang.reflect.Type;
import java.util.function.Function;
import graphql.schema.DataFetchingEnvironment;
@@ -84,22 +85,25 @@ class RepositoryUtils {
return CursorStrategy.withEncoder(new ScrollPositionCursorStrategy(), CursorEncoder.base64());
}
public static int defaultScrollCount() {
return 20;
}
public static Function<Boolean, ScrollPosition> defaultScrollPosition() {
return forward -> ScrollPosition.offset();
}
public static ScrollSubrange defaultScrollSubrange() {
return ScrollSubrange.create(ScrollPosition.offset(), 20, true);
}
public static ScrollSubrange getScrollSubrange(
DataFetchingEnvironment env, CursorStrategy<ScrollPosition> strategy,
ScrollSubrange defaultSubrange) {
DataFetchingEnvironment env, CursorStrategy<ScrollPosition> strategy) {
boolean forward = !env.getArguments().containsKey("last");
Integer count = env.getArgument(forward ? "first" : "last");
count = (count != null ? count : defaultSubrange.count().getAsInt());
String cursor = env.getArgument(forward ? "after" : "before");
ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : defaultSubrange.position().get());
ScrollPosition position = (cursor != null ? strategy.fromCursor(cursor) : null);
return ScrollSubrange.create(position, count, forward);
}

View File

@@ -37,8 +37,6 @@ public class RepositoryUtilsTests {
private final CursorStrategy<ScrollPosition> cursorStrategy = RepositoryUtils.defaultCursorStrategy();
private final ScrollSubrange defaultSubrange = ScrollSubrange.create(ScrollPosition.offset(50), 20, true);
@Test
void buildScrollSubrangeForward() {
@@ -48,7 +46,7 @@ public class RepositoryUtilsTests {
DataFetchingEnvironment env = environment(
Map.of("first", count, "after", cursorStrategy.toCursor(offset)));
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(11));
assertThat(range.count().getAsInt()).isEqualTo(count);
@@ -63,7 +61,7 @@ public class RepositoryUtilsTests {
DataFetchingEnvironment env = environment(
Map.of("last", count, "before", cursorStrategy.toCursor(offset)));
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
assertThat(range.position().get()).isEqualTo(ScrollPosition.offset(5));
assertThat(range.count().getAsInt()).isEqualTo(count);
@@ -71,34 +69,41 @@ public class RepositoryUtilsTests {
}
@Test
void buildScrollSubrangeForwardWithDefaultScrollSubrange() {
void noInput() {
DataFetchingEnvironment env = environment(Collections.emptyMap());
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(1));
assertThat(range.count().getAsInt()).isEqualTo(this.defaultSubrange.count().getAsInt());
assertThat(range.position()).isNotPresent();
assertThat(range.count()).isNotPresent();
assertThat(range.forward()).isTrue();
}
@Test // gh-900
void buildScrollSubrangeBackwardFromDefaultPosition() {
@Test
void buildScrollSubrangeForwardWithoutPosition() {
int count = 5;
DataFetchingEnvironment env = environment(Map.of("last", count));
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy, defaultSubrange);
DataFetchingEnvironment env = environment(Map.of("first", count));
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
assertThat(range.position().get()).isEqualTo(getDefaultPosition().advanceBy(-count));
assertThat(range.position()).isNotPresent();
assertThat(range.count().getAsInt()).isEqualTo(count);
assertThat(range.forward()).isTrue();
}
@Test
void buildScrollSubrangeBackwardWithoutPosition() {
int count = 5;
DataFetchingEnvironment env = environment(Map.of("last", count));
ScrollSubrange range = RepositoryUtils.getScrollSubrange(env, cursorStrategy);
assertThat(range.position()).isNotPresent();
assertThat(range.count().getAsInt()).isEqualTo(count);
assertThat(range.forward()).isFalse();
}
private static DataFetchingEnvironment environment(Map<String, Object> arguments) {
return DataFetchingEnvironmentImpl.newDataFetchingEnvironment()
.arguments(arguments)
.build();
}
private OffsetScrollPosition getDefaultPosition() {
return (OffsetScrollPosition) defaultSubrange.position().get();
}
}