Browse Source

Add `WindowIterator` and rename `Scroll` to `Window`.

The intend of WindowIterator is to support users who need to iterate multiple windows. It keeps track of the position and loads the next window if needed so that the user does not have to interact with the position at all.

Also remove the Window methods to get the first/last position and enforce the index based variant.

Update the documentation to make use of the newly introduced API.

See: #2151
Original Pull Request: #2787
pull/2794/head
Christoph Strobl 3 years ago
parent
commit
0c0c1afc8e
No known key found for this signature in database
GPG Key ID: 8CC1AB53391458C8
  1. 73
      src/main/asciidoc/repositories-scrolling.adoc
  2. 78
      src/main/java/org/springframework/data/domain/Window.java
  3. 12
      src/main/java/org/springframework/data/domain/WindowImpl.java
  4. 117
      src/main/java/org/springframework/data/domain/WindowIterator.java
  5. 10
      src/main/java/org/springframework/data/repository/query/FluentQuery.java
  6. 8
      src/main/java/org/springframework/data/repository/query/QueryMethod.java
  7. 6
      src/main/java/org/springframework/data/repository/query/ResultProcessor.java
  8. 4
      src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java
  9. 131
      src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java
  10. 25
      src/test/java/org/springframework/data/domain/WindowUnitTests.java
  11. 4
      src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java
  12. 10
      src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java

73
src/main/asciidoc/repositories-scrolling.adoc

@ -6,8 +6,36 @@ Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scro
You can define simple sorting expressions by using property names and define static result limiting using the <<repositories.limit-query-result,`Top` or `First` keyword>> through query derivation. You can define simple sorting expressions by using property names and define static result limiting using the <<repositories.limit-query-result,`Top` or `First` keyword>> through query derivation.
You can concatenate expressions to collect multiple criteria into one expression. You can concatenate expressions to collect multiple criteria into one expression.
Scroll queries return a `Scroll<T>` that allows obtaining the scroll position to resume to obtain the next `Scroll<T>` until your application has consumed the entire query result. Scroll queries return a `Window<T>` that allows obtaining the scroll position to resume to obtain the next `Window<T>` until your application has consumed the entire query result.
Similar to consuming a Java `Iterator<List<…>>` by obtaining the next batch of results, query result scrolling lets you access the next `ScrollPosition` through `Scroll.lastScrollPosition()`. Similar to consuming a Java `Iterator<List<…>>` by obtaining the next batch of results, query result scrolling lets you access the a `ScrollPosition` through `Window.positionAt(...)`.
[source,java]
----
Window<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial());
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.positionAt(users.size() - 1));
} while (!users.isEmpty() && !users.isLast());
----
`WindowIterator` provides a specialized implementation that simplifies consumption of multiple `Window` instances by removing the need to check for the presence of a next `Window` and applying the `ScrollPosition`.
[source,java]
----
WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(OffsetScrollPosition.initial());
while (users.hasNext()) {
users.next().forEach(user -> {
// consume the user
});
}
----
[[repositories.scrolling.offset]] [[repositories.scrolling.offset]]
===== Scrolling using Offset ===== Scrolling using Offset
@ -22,21 +50,13 @@ However, most databases require materializing the full query result before your
---- ----
interface UserRepository extends Repository<User, Long> { interface UserRepository extends Repository<User, Long> {
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position); Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
} }
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial()); WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(OffsetScrollPosition.initial()); <1>
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
} while (!users.isEmpty() && !users.isLast());
---- ----
<1> Start from the initial offset at position 0;
==== ====
[[repositories.scrolling.keyset]] [[repositories.scrolling.keyset]]
@ -50,31 +70,30 @@ This approach maintains a set of keys to resume scrolling by passing keys into t
The core idea of Keyset-Filtering is to start retrieving results using a stable sorting order. The core idea of Keyset-Filtering is to start retrieving results using a stable sorting order.
Once you want to scroll to the next chunk, you obtain a `ScrollPosition` that is used to reconstruct the position within the sorted result. Once you want to scroll to the next chunk, you obtain a `ScrollPosition` that is used to reconstruct the position within the sorted result.
The `ScrollPosition` captures the keyset of the last entity within the current `Scroll`. The `ScrollPosition` captures the keyset of the last entity within the current `Window`.
To run the query, reconstruction rewrites the criteria clause to include all sort fields and the primary key so that the database can leverage potential indexes to run the query. To run the query, reconstruction rewrites the criteria clause to include all sort fields and the primary key so that the database can leverage potential indexes to run the query.
The database needs only constructing a much smaller result from the given keyset position without the need to fully materialize a large result and then skipping results until reaching a particular offset. The database needs only constructing a much smaller result from the given keyset position without the need to fully materialize a large result and then skipping results until reaching a particular offset.
[WARNING]
====
Keyset-Filtering requires properties used for sorting to be non nullable.
This limitation applies due to the store specific `null` value handling of comparison operators as well as the need to run queries against an indexed source.
Keyset-Filtering on nullable properties will lead to unexpected results.
====
.Using `KeysetScrollPosition` with Repository Query Methods .Using `KeysetScrollPosition` with Repository Query Methods
==== ====
[source,java] [source,java]
---- ----
interface UserRepository extends Repository<User, Long> { interface UserRepository extends Repository<User, Long> {
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position); Window<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
} }
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", KeysetScrollPosition.initial()); WindowIterator<User> users = WindowIterator.of(position -> repository.findFirst10ByLastnameOrderByFirstname("Doe", position))
.startingAt(KeysetScrollPosition.initial()); <1>
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
} while (!users.isEmpty() && !users.isLast());
---- ----
<1> Start at the very beginning and do not apply additional filtering.
==== ====
Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well. Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well.

78
src/main/java/org/springframework/data/domain/Scroll.java → src/main/java/org/springframework/data/domain/Window.java

@ -23,65 +23,66 @@ import java.util.function.IntFunction;
import org.springframework.data.util.Streamable; import org.springframework.data.util.Streamable;
/** /**
* A scroll of data consumed from an underlying query result. A scroll is similar to {@link Slice} in the sense that it * A set of data consumed from an underlying query result. A {@link Window} is similar to {@link Slice} in the sense
* contains a subset of the actual query results for easier consumption of large result sets. The scroll is less * that it contains a subset of the actual query results for easier consumption of large result sets. The window is less
* opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or * opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or
* cursor resume tokens. * cursor resume tokens.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Christoph Strobl
* @since 3.1 * @since 3.1
* @see ScrollPosition * @see ScrollPosition
*/ */
public interface Scroll<T> extends Streamable<T> { public interface Window<T> extends Streamable<T> {
/** /**
* Construct a {@link Scroll}. * Construct a {@link Window}.
* *
* @param items the list of data. * @param items the list of data.
* @param positionFunction the list of data. * @param positionFunction the list of data.
* @return the {@link Scroll}. * @return the {@link Window}.
* @param <T> * @param <T>
*/ */
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) { static <T> Window<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) {
return new ScrollImpl<>(items, positionFunction, false); return new WindowImpl<>(items, positionFunction, false);
} }
/** /**
* Construct a {@link Scroll}. * Construct a {@link Window}.
* *
* @param items the list of data. * @param items the list of data.
* @param positionFunction the list of data. * @param positionFunction the list of data.
* @param hasNext * @param hasNext
* @return the {@link Scroll}. * @return the {@link Window}.
* @param <T> * @param <T>
*/ */
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) { static <T> Window<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
return new ScrollImpl<>(items, positionFunction, hasNext); return new WindowImpl<>(items, positionFunction, hasNext);
} }
/** /**
* Returns the number of elements in this scroll. * Returns the number of elements in this window.
* *
* @return the number of elements in this scroll. * @return the number of elements in this window.
*/ */
int size(); int size();
/** /**
* Returns {@code true} if this scroll contains no elements. * Returns {@code true} if this window contains no elements.
* *
* @return {@code true} if this scroll contains no elements * @return {@code true} if this window contains no elements
*/ */
boolean isEmpty(); boolean isEmpty();
/** /**
* Returns the scroll content as {@link List}. * Returns the windows content as {@link List}.
* *
* @return * @return
*/ */
List<T> getContent(); List<T> getContent();
/** /**
* Returns whether the current scroll is the last one. * Returns whether the current window is the last one.
* *
* @return * @return
*/ */
@ -90,9 +91,9 @@ public interface Scroll<T> extends Streamable<T> {
} }
/** /**
* Returns if there is a next scroll. * Returns if there is a next window.
* *
* @return if there is a next scroll window. * @return if there is a next window window.
*/ */
boolean hasNext(); boolean hasNext();
@ -123,45 +124,12 @@ public interface Scroll<T> extends Streamable<T> {
return positionAt(index); return positionAt(index);
} }
// TODO: First and last seem to conflict with first/last scroll or first/last position of elements.
// these methods should rather express the position of the first element within this scroll and the scroll position
// to be used to get the next Scroll.
/** /**
* Returns the first {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty. * Returns a new {@link Window} with the content of the current one mapped by the given {@code converter}.
*
* @return the first {@link ScrollPosition}.
* @throws NoSuchElementException if this result is empty.
*/
default ScrollPosition firstPosition() {
if (size() == 0) {
throw new NoSuchElementException();
}
return positionAt(0);
}
/**
* Returns the last {@link ScrollPosition} or throw {@link NoSuchElementException} if the list is empty.
*
* @return the last {@link ScrollPosition}.
* @throws NoSuchElementException if this result is empty.
*/
default ScrollPosition lastPosition() {
if (size() == 0) {
throw new NoSuchElementException();
}
return positionAt(size() - 1);
}
/**
* Returns a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
* *
* @param converter must not be {@literal null}. * @param converter must not be {@literal null}.
* @return a new {@link Scroll} with the content of the current one mapped by the given {@code converter}. * @return a new {@link Window} with the content of the current one mapped by the given {@code converter}.
*/ */
<U> Scroll<U> map(Function<? super T, ? extends U> converter); <U> Window<U> map(Function<? super T, ? extends U> converter);
} }

12
src/main/java/org/springframework/data/domain/ScrollImpl.java → src/main/java/org/springframework/data/domain/WindowImpl.java

@ -26,19 +26,19 @@ import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
/** /**
* Default {@link Scroll} implementation. * Default {@link Window} implementation.
* *
* @author Mark Paluch * @author Mark Paluch
* @since 3.1 * @since 3.1
*/ */
class ScrollImpl<T> implements Scroll<T> { class WindowImpl<T> implements Window<T> {
private final List<T> items; private final List<T> items;
private final IntFunction<? extends ScrollPosition> positionFunction; private final IntFunction<? extends ScrollPosition> positionFunction;
private final boolean hasNext; private final boolean hasNext;
ScrollImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) { WindowImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
Assert.notNull(items, "List of items must not be null"); Assert.notNull(items, "List of items must not be null");
Assert.notNull(positionFunction, "Position function must not be null"); Assert.notNull(positionFunction, "Position function must not be null");
@ -79,11 +79,11 @@ class ScrollImpl<T> implements Scroll<T> {
} }
@Override @Override
public <U> Scroll<U> map(Function<? super T, ? extends U> converter) { public <U> Window<U> map(Function<? super T, ? extends U> converter) {
Assert.notNull(converter, "Function must not be null"); Assert.notNull(converter, "Function must not be null");
return new ScrollImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext); return new WindowImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
} }
@NotNull @NotNull
@ -98,7 +98,7 @@ class ScrollImpl<T> implements Scroll<T> {
return true; return true;
if (o == null || getClass() != o.getClass()) if (o == null || getClass() != o.getClass())
return false; return false;
ScrollImpl<?> that = (ScrollImpl<?>) o; WindowImpl<?> that = (WindowImpl<?>) o;
return ObjectUtils.nullSafeEquals(items, that.items) return ObjectUtils.nullSafeEquals(items, that.items)
&& ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction) && ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction)
&& ObjectUtils.nullSafeEquals(hasNext, that.hasNext); && ObjectUtils.nullSafeEquals(hasNext, that.hasNext);

117
src/main/java/org/springframework/data/domain/WindowIterator.java

@ -0,0 +1,117 @@
/*
* 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.domain;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.function.Function;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
/**
* An {@link Iterator} over multiple {@link Window Windows} obtained via a {@link Function window function}, that keeps track of
* the current {@link ScrollPosition} returning the Window {@link Window#getContent() content} on {@link #next()}.
* <pre class="code">
* WindowIterator&lt;User&gt; users = WindowIterator.of(position -> repository.findFirst10By...("spring", position))
* .startingAt(OffsetScrollPosition.initial());
* while (users.hasNext()) {
* users.next().forEach(user -> {
* // consume the user
* });
* }
* </pre>
*
* @author Christoph Strobl
* @since 3.1
*/
public class WindowIterator<T> implements Iterator<List<T>> {
private final Function<ScrollPosition, Window<T>> windowFunction;
private ScrollPosition currentPosition;
@Nullable //
private Window<T> currentWindow;
/**
* Entrypoint to create a new {@link WindowIterator} for the given windowFunction.
*
* @param windowFunction must not be {@literal null}.
* @param <T>
* @return new instance of {@link WindowIteratorBuilder}.
*/
public static <T> WindowIteratorBuilder<T> of(Function<ScrollPosition, Window<T>> windowFunction) {
return new WindowIteratorBuilder(windowFunction);
}
WindowIterator(Function<ScrollPosition, Window<T>> windowFunction, ScrollPosition position) {
this.windowFunction = windowFunction;
this.currentPosition = position;
this.currentWindow = doScroll();
}
@Override
public boolean hasNext() {
return currentWindow != null;
}
@Override
public List<T> next() {
List<T> toReturn = new ArrayList<>(currentWindow.getContent());
currentPosition = currentWindow.positionAt(currentWindow.size() -1);
currentWindow = doScroll();
return toReturn;
}
@Nullable
Window<T> doScroll() {
if (currentWindow != null && !currentWindow.hasNext()) {
return null;
}
Window<T> window = windowFunction.apply(currentPosition);
if (window.isEmpty() && window.isLast()) {
return null;
}
return window;
}
/**
* Builder API to construct a {@link WindowIterator}.
*
* @param <T>
* @author Christoph Strobl
* @since 3.1
*/
public static class WindowIteratorBuilder<T> {
private Function<ScrollPosition, Window<T>> windowFunction;
WindowIteratorBuilder(Function<ScrollPosition, Window<T>> windowFunction) {
this.windowFunction = windowFunction;
}
public WindowIterator<T> startingAt(ScrollPosition position) {
Assert.state(windowFunction != null, "WindowFunction cannot not be null");
return new WindowIterator<>(windowFunction, position);
}
}
}

10
src/main/java/org/springframework/data/repository/query/FluentQuery.java

@ -15,6 +15,7 @@
*/ */
package org.springframework.data.repository.query; package org.springframework.data.repository.query;
import org.springframework.data.domain.Window;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -26,7 +27,6 @@ import java.util.stream.Stream;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -165,7 +165,7 @@ public interface FluentQuery<T> {
List<T> all(); List<T> all();
/** /**
* Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at
* {@code scrollPosition}. * {@code scrollPosition}.
* *
* @param scrollPosition must not be {@literal null}. * @param scrollPosition must not be {@literal null}.
@ -174,7 +174,7 @@ public interface FluentQuery<T> {
* @throws UnsupportedOperationException if not supported by the underlying implementation. * @throws UnsupportedOperationException if not supported by the underlying implementation.
* @since 3.1 * @since 3.1
*/ */
default Scroll<T> scroll(ScrollPosition scrollPosition) { default Window<T> scroll(ScrollPosition scrollPosition) {
throw new UnsupportedOperationException("Scrolling not supported"); throw new UnsupportedOperationException("Scrolling not supported");
} }
@ -261,7 +261,7 @@ public interface FluentQuery<T> {
Flux<T> all(); Flux<T> all();
/** /**
* Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at * Get all matching elements as {@link Window} to start result scrolling or resume scrolling at
* {@code scrollPosition}. * {@code scrollPosition}.
* *
* @param scrollPosition must not be {@literal null}. * @param scrollPosition must not be {@literal null}.
@ -270,7 +270,7 @@ public interface FluentQuery<T> {
* @throws UnsupportedOperationException if not supported by the underlying implementation. * @throws UnsupportedOperationException if not supported by the underlying implementation.
* @since 3.1 * @since 3.1
*/ */
default Mono<Scroll<T>> scroll(ScrollPosition scrollPosition) { default Mono<Window<T>> scroll(ScrollPosition scrollPosition) {
throw new UnsupportedOperationException("Scrolling not supported"); throw new UnsupportedOperationException("Scrolling not supported");
} }

8
src/main/java/org/springframework/data/repository/query/QueryMethod.java

@ -24,7 +24,7 @@ import java.util.stream.Stream;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll; import org.springframework.data.domain.Window;
import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
@ -97,7 +97,7 @@ public class QueryMethod {
} }
if (hasParameterOfType(method, ScrollPosition.class)) { if (hasParameterOfType(method, ScrollPosition.class)) {
assertReturnTypeAssignable(method, Collections.singleton(Scroll.class)); assertReturnTypeAssignable(method, Collections.singleton(Window.class));
} }
Assert.notNull(this.parameters, Assert.notNull(this.parameters,
@ -203,13 +203,13 @@ public class QueryMethod {
} }
/** /**
* Returns whether the query method will return a {@link Scroll}. * Returns whether the query method will return a {@link Window}.
* *
* @return * @return
* @since 3.1 * @since 3.1
*/ */
public boolean isScrollQuery() { public boolean isScrollQuery() {
return org.springframework.util.ClassUtils.isAssignable(Scroll.class, unwrappedReturnType); return org.springframework.util.ClassUtils.isAssignable(Window.class, unwrappedReturnType);
} }
/** /**

6
src/main/java/org/springframework/data/repository/query/ResultProcessor.java

@ -26,7 +26,7 @@ import org.springframework.core.CollectionFactory;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Scroll; import org.springframework.data.domain.Window;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.repository.util.ReactiveWrapperConverters; import org.springframework.data.repository.util.ReactiveWrapperConverters;
@ -144,8 +144,8 @@ public class ResultProcessor {
ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter); ChainingConverter converter = ChainingConverter.of(type.getReturnedType(), preparingConverter).and(this.converter);
if (source instanceof Scroll<?> && method.isScrollQuery()) { if (source instanceof Window<?> && method.isScrollQuery()) {
return (T) ((Scroll<?>) source).map(converter::convert); return (T) ((Window<?>) source).map(converter::convert);
} }
if (source instanceof Slice && (method.isPageQuery() || method.isSliceQuery())) { if (source instanceof Slice && (method.isPageQuery() || method.isSliceQuery())) {

4
src/main/java/org/springframework/data/repository/util/QueryExecutionConverters.java

@ -36,7 +36,7 @@ import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.convert.support.ConfigurableConversionService; import org.springframework.core.convert.support.ConfigurableConversionService;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Scroll; import org.springframework.data.domain.Window;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.GeoResults;
import org.springframework.data.util.CustomCollections; import org.springframework.data.util.CustomCollections;
@ -98,7 +98,7 @@ public abstract class QueryExecutionConverters {
ALLOWED_PAGEABLE_TYPES.add(Slice.class); ALLOWED_PAGEABLE_TYPES.add(Slice.class);
ALLOWED_PAGEABLE_TYPES.add(Page.class); ALLOWED_PAGEABLE_TYPES.add(Page.class);
ALLOWED_PAGEABLE_TYPES.add(List.class); ALLOWED_PAGEABLE_TYPES.add(List.class);
ALLOWED_PAGEABLE_TYPES.add(Scroll.class); ALLOWED_PAGEABLE_TYPES.add(Window.class);
WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType()); WRAPPER_TYPES.add(NullableWrapperToCompletableFutureConverter.getWrapperType());

131
src/test/java/org/springframework/data/domain/WindowIteratorUnitTests.java

@ -0,0 +1,131 @@
/*
* 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.domain;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.data.domain.WindowIterator.WindowIteratorBuilder;
/**
* @author Christoph Strobl
*/
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
class WindowIteratorUnitTests<T> {
@Mock Function<ScrollPosition, Window<T>> fkt;
@Mock Window<T> window;
@Mock ScrollPosition scrollPosition;
@Captor ArgumentCaptor<ScrollPosition> scrollCaptor;
@BeforeEach
void beforeEach() {
when(fkt.apply(any())).thenReturn(window);
}
@Test // GH-2151
void loadsDataOnCreation() {
WindowIteratorBuilder<T> of = WindowIterator.of(fkt);
verifyNoInteractions(fkt);
of.startingAt(scrollPosition);
verify(fkt).apply(eq(scrollPosition));
}
@Test // GH-2151
void hasNextReturnsFalseIfNoDataAvailable() {
when(window.isLast()).thenReturn(true);
when(window.isEmpty()).thenReturn(true);
assertThat(WindowIterator.of(fkt).startingAt(scrollPosition).hasNext()).isFalse();
}
@Test // GH-2151
void hasNextReturnsTrueIfDataAvailableButOnlyOnePage() {
when(window.isLast()).thenReturn(true);
when(window.isEmpty()).thenReturn(false);
assertThat(WindowIterator.of(fkt).startingAt(scrollPosition).hasNext()).isTrue();
}
@Test // GH-2151
void allowsToIterateAllWindows() {
ScrollPosition p1 = mock(ScrollPosition.class);
ScrollPosition p2 = mock(ScrollPosition.class);
when(window.isEmpty()).thenReturn(false, false, false);
when(window.isLast()).thenReturn(false, false, true);
when(window.hasNext()).thenReturn(true, true, false);
when(window.size()).thenReturn(1, 1, 1);
when(window.positionAt(anyInt())).thenReturn(p1, p2);
when(window.getContent()).thenReturn(List.of((T) "0"), List.of((T) "1"), List.of((T) "2"));
WindowIterator<T> iterator = WindowIterator.of(fkt).startingAt(scrollPosition);
List<T> capturedResult = new ArrayList<>(3);
while (iterator.hasNext()) {
capturedResult.addAll(iterator.next());
}
verify(fkt, times(3)).apply(scrollCaptor.capture());
assertThat(scrollCaptor.getAllValues()).containsExactly(scrollPosition, p1, p2);
assertThat(capturedResult).containsExactly((T) "0", (T) "1", (T) "2");
}
@Test // GH-2151
void stopsAfterFirstPageIfOnlyOneWindowAvailable() {
ScrollPosition p1 = mock(ScrollPosition.class);
when(window.isEmpty()).thenReturn(false);
when(window.isLast()).thenReturn(true);
when(window.hasNext()).thenReturn(false);
when(window.size()).thenReturn(1);
when(window.positionAt(anyInt())).thenReturn(p1);
when(window.getContent()).thenReturn(List.of((T) "0"));
WindowIterator<T> iterator = WindowIterator.of(fkt).startingAt(scrollPosition);
List<T> capturedResult = new ArrayList<>(1);
while (iterator.hasNext()) {
capturedResult.addAll(iterator.next());
}
verify(fkt).apply(scrollCaptor.capture());
assertThat(scrollCaptor.getAllValues()).containsExactly(scrollPosition);
assertThat(capturedResult).containsExactly((T) "0");
}
}

25
src/test/java/org/springframework/data/domain/ScrollUnitTests.java → src/test/java/org/springframework/data/domain/WindowUnitTests.java

@ -23,31 +23,32 @@ import java.util.function.IntFunction;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
* Unit tests for {@link Scroll}. * Unit tests for {@link Window}.
* *
* @author Mark Paluch * @author Mark Paluch
* @author Christoph Strobl
*/ */
class ScrollUnitTests { class WindowUnitTests {
@Test // GH-2151 @Test // GH-2151
void equalsAndHashCode() { void equalsAndHashCode() {
IntFunction<OffsetScrollPosition> positionFunction = OffsetScrollPosition.positionFunction(0); IntFunction<OffsetScrollPosition> positionFunction = OffsetScrollPosition.positionFunction(0);
Scroll<Integer> one = Scroll.from(List.of(1, 2, 3), positionFunction); Window<Integer> one = Window.from(List.of(1, 2, 3), positionFunction);
Scroll<Integer> two = Scroll.from(List.of(1, 2, 3), positionFunction); Window<Integer> two = Window.from(List.of(1, 2, 3), positionFunction);
assertThat(one).isEqualTo(two).hasSameHashCodeAs(two); assertThat(one).isEqualTo(two).hasSameHashCodeAs(two);
assertThat(one.equals(two)).isTrue(); assertThat(one.equals(two)).isTrue();
assertThat(Scroll.from(List.of(1, 2, 3), positionFunction, true)).isNotEqualTo(two).doesNotHaveSameHashCodeAs(two); assertThat(Window.from(List.of(1, 2, 3), positionFunction, true)).isNotEqualTo(two).doesNotHaveSameHashCodeAs(two);
} }
@Test // GH-2151 @Test // GH-2151
void allowsIteration() { void allowsIteration() {
Scroll<Integer> scroll = Scroll.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); Window<Integer> window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0));
for (Integer integer : scroll) { for (Integer integer : window) {
assertThat(integer).isBetween(1, 3); assertThat(integer).isBetween(1, 3);
} }
} }
@ -55,15 +56,15 @@ class ScrollUnitTests {
@Test // GH-2151 @Test // GH-2151
void shouldCreateCorrectPositions() { void shouldCreateCorrectPositions() {
Scroll<Integer> scroll = Scroll.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0)); Window<Integer> window = Window.from(List.of(1, 2, 3), OffsetScrollPosition.positionFunction(0));
assertThat(scroll.firstPosition()).isEqualTo(OffsetScrollPosition.of(1)); assertThat(window.positionAt(0)).isEqualTo(OffsetScrollPosition.of(1));
assertThat(scroll.lastPosition()).isEqualTo(OffsetScrollPosition.of(3)); assertThat(window.positionAt(window.size() - 1)).isEqualTo(OffsetScrollPosition.of(3));
// by index // by index
assertThat(scroll.positionAt(1)).isEqualTo(OffsetScrollPosition.of(2)); assertThat(window.positionAt(1)).isEqualTo(OffsetScrollPosition.of(2));
// by object // by object
assertThat(scroll.positionAt(Integer.valueOf(1))).isEqualTo(OffsetScrollPosition.of(1)); assertThat(window.positionAt(Integer.valueOf(1))).isEqualTo(OffsetScrollPosition.of(1));
} }
} }

4
src/test/java/org/springframework/data/repository/query/ParametersUnitTests.java

@ -28,7 +28,7 @@ import org.reactivestreams.Publisher;
import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll; import org.springframework.data.domain.Window;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.test.util.ReflectionTestUtils; import org.springframework.test.util.ReflectionTestUtils;
@ -231,7 +231,7 @@ class ParametersUnitTests {
Page<Object> customPageable(SomePageable pageable); Page<Object> customPageable(SomePageable pageable);
Scroll<Object> customScrollPosition(OffsetScrollPosition request); Window<Object> customScrollPosition(OffsetScrollPosition request);
} }
interface SomePageable extends Pageable {} interface SomePageable extends Pageable {}

10
src/test/java/org/springframework/data/repository/query/QueryMethodUnitTests.java

@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import io.vavr.collection.Seq; import io.vavr.collection.Seq;
import io.vavr.control.Option; import io.vavr.control.Option;
import org.springframework.data.domain.Window;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@ -32,7 +33,6 @@ import org.eclipse.collections.api.list.ImmutableList;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page; import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice; import org.springframework.data.domain.Slice;
import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.ProjectionFactory;
@ -351,15 +351,15 @@ class QueryMethodUnitTests {
ImmutableList<User> returnsEclipseCollection(); ImmutableList<User> returnsEclipseCollection();
Scroll<User> cursorWindow(ScrollPosition cursorRequest); Window<User> cursorWindow(ScrollPosition cursorRequest);
Mono<Scroll<User>> reactiveCursorWindow(ScrollPosition cursorRequest); Mono<Window<User>> reactiveCursorWindow(ScrollPosition cursorRequest);
Flux<Scroll<User>> invalidReactiveCursorWindow(ScrollPosition cursorRequest); Flux<Window<User>> invalidReactiveCursorWindow(ScrollPosition cursorRequest);
Page<User> cursorWindowMethodWithInvalidReturnType(ScrollPosition cursorRequest); Page<User> cursorWindowMethodWithInvalidReturnType(ScrollPosition cursorRequest);
Scroll<User> cursorWindowWithoutScrollPosition(); Window<User> cursorWindowWithoutScrollPosition();
} }
class User { class User {

Loading…
Cancel
Save