Introduce Scroll API.

See: #2151
Original Pull Request: #2787
This commit is contained in:
Mark Paluch
2023-02-27 13:37:06 +01:00
committed by Christoph Strobl
parent a5de842460
commit 035965a3a2
27 changed files with 1280 additions and 134 deletions

View File

@@ -2,6 +2,7 @@
Oliver Gierke; Thomas Darimont; Christoph Strobl; Mark Pollack; Thomas Risberg; Mark Paluch; Jay Bryant
:revnumber: {version}
:revdate: {localdate}
:feature-scroll: true
ifdef::backend-epub3[:front-cover-image: image:epub-cover.png[Front Cover,1050,1600]]
(C) 2008-2022 The original authors.

View File

@@ -0,0 +1,136 @@
[[repositories.special-parameters]]
=== Special parameter handling
To handle parameters in your query, define method parameters as already seen in the preceding examples.
Besides that, the infrastructure recognizes certain specific types like `Pageable` and `Sort`, to apply pagination and sorting to your queries dynamically.
The following example demonstrates these features:
ifdef::feature-scroll[]
.Using `Pageable`, `Slice`, `ScrollPosition`, and `Sort` in query methods
====
[source,java]
----
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
Scroll<User> findTop10ByLastname(String lastname, ScrollPosition position, Sort sort);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
----
====
endif::[]
ifndef::feature-scroll[]
.Using `Pageable`, `Slice`, and `Sort` in query methods
====
[source,java]
----
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
----
====
endif::[]
IMPORTANT: APIs taking `Sort` and `Pageable` expect non-`null` values to be handed into methods.
If you do not want to apply any sorting or pagination, use `Sort.unsorted()` and `Pageable.unpaged()`.
The first method lets you pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query.
A `Page` knows about the total number of elements and pages available.
It does so by the infrastructure triggering a count query to calculate the overall number.
As this might be expensive (depending on the store used), you can instead return a `Slice`.
A `Slice` knows only about whether a next `Slice` is available, which might be sufficient when walking through a larger result set.
Sorting options are handled through the `Pageable` instance, too.
If you need only sorting, add an `org.springframework.data.domain.Sort` parameter to your method.
As you can see, returning a `List` is also possible.
In this case, the additional metadata required to build the actual `Page` instance is not created (which, in turn, means that the additional count query that would have been necessary is not issued).
Rather, it restricts the query to look up only the given range of entities.
NOTE: To find out how many pages you get for an entire query, you have to trigger an additional count query.
By default, this query is derived from the query you actually trigger.
[[repositories.paging-and-sorting]]
==== Paging and Sorting
You can define simple sorting expressions by using property names.
You can concatenate expressions to collect multiple criteria into one expression.
.Defining sort expressions
====
[source,java]
----
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
----
====
For a more type-safe way to define sort expressions, start with the type for which to define the sort expression and use method references to define the properties on which to sort.
.Defining sort expressions by using the type-safe API
====
[source,java]
----
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
----
====
NOTE: `TypedSort.by(…)` makes use of runtime proxies by (typically) using CGlib, which may interfere with native image compilation when using tools such as Graal VM Native.
If your store implementation supports Querydsl, you can also use the generated metamodel types to define sort expressions:
.Defining sort expressions by using the Querydsl API
====
[source,java]
----
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
----
====
ifdef::feature-scroll[]
include::repositories-scrolling.adoc[]
endif::[]
[[repositories.limit-query-result]]
=== Limiting Query Results
You can limit the results of query methods by using the `first` or `top` keywords, which you can use interchangeably.
You can append an optional numeric value to `top` or `first` to specify the maximum result size to be returned.
If the number is left out, a result size of 1 is assumed.
The following example shows how to limit the query size:
.Limiting the result size of a query with `Top` and `First`
====
[source,java]
----
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
----
====
The limiting expressions also support the `Distinct` keyword for datastores that support distinct queries.
Also, for the queries that limit the result set to one instance, wrapping the result into with the `Optional` keyword is supported.
If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of available pages), it is applied within the limited result.
NOTE: Limiting the results in combination with dynamic sorting by using a `Sort` parameter lets you express query methods for the 'K' smallest as well as for the 'K' biggest elements.

View File

@@ -0,0 +1,83 @@
[[repositories.scrolling]]
==== Scrolling
Scrolling is a more fine-grained approach to iterate through larger results set chunks.
Scrolling consists of a stable sort, a scroll type (Offset- or Keyset-based scrolling) and result limiting.
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.
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.
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()`.
[[repositories.scrolling.offset]]
===== Scrolling using Offset
Offset scrolling uses similar to pagination, an Offset counter to skip a number of results and let the data source only return results beginning at the given Offset.
This simple mechanism avoids large results being sent to the client application.
However, most databases require materializing the full query result before your server can return the results.
.Using `OffsetScrollPosition` with Repository Query Methods
====
[source,java]
----
interface UserRepository extends Repository<User, Long> {
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, OffsetScrollPosition position);
}
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", OffsetScrollPosition.initial());
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
} while (!users.isEmpty() && !users.isLast());
----
====
[[repositories.scrolling.keyset]]
===== Scrolling using Keyset-Filtering
Offset-based requires most databases require materializing the entire result before your server can return the results.
So while the client only sees the portion of the requested results, your server needs to build the full result, which causes additional load.
Keyset-Filtering approaches result subset retrieval by leveraging built-in capabilities of your database aiming to reduce the computation and I/O requirements for individual queries.
This approach maintains a set of keys to resume scrolling by passing keys into the query, effectively amending your filter criteria.
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.
The `ScrollPosition` captures the keyset of the last entity within the current `Scroll`.
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.
.Using `KeysetScrollPosition` with Repository Query Methods
====
[source,java]
----
interface UserRepository extends Repository<User, Long> {
Scroll<User> findFirst10ByLastnameOrderByFirstname(String lastname, KeysetScrollPosition position);
}
Scroll<User> users = repository.findFirst10ByLastnameOrderByFirstname("Doe", KeysetScrollPosition.initial());
do {
for (User u : users) {
// consume the user
}
// obtain the next Scroll
users = repository.findFirst10ByLastnameOrderByFirstname("Doe", users.lastScrollPosition());
} while (!users.isEmpty() && !users.isLast());
----
====
Keyset-Filtering works best when your database contains an index that matches the sort fields, hence a static sort works well.
Scroll queries applying Keyset-Filtering require to the properties used in the sort order to be returned by the query, and these must be mapped in the returned entity.
You can use interface and DTO projections, however make sure to include all properties that you've sorted by to avoid keyset extraction failures.

View File

@@ -92,6 +92,10 @@ Page<User> users = repository.findAll(PageRequest.of(1, 20));
----
====
ifdef::feature-scroll[]
In addition to pagination, scrolling provides a more fine-grained access to iterate through chunks of larger result sets.
endif::[]
In addition to query methods, query derivation for both count and delete queries is available.
The following list shows the interface definition for a derived count query:
@@ -517,118 +521,7 @@ List<Person> findByAddress_ZipCode(ZipCode zipCode);
Because we treat the underscore character as a reserved character, we strongly advise following standard Java naming conventions (that is, not using underscores in property names but using camel case instead).
[[repositories.special-parameters]]
=== Special parameter handling
To handle parameters in your query, define method parameters as already seen in the preceding examples.
Besides that, the infrastructure recognizes certain specific types like `Pageable` and `Sort`, to apply pagination and sorting to your queries dynamically.
The following example demonstrates these features:
.Using `Pageable`, `Slice`, and `Sort` in query methods
====
[source,java]
----
Page<User> findByLastname(String lastname, Pageable pageable);
Slice<User> findByLastname(String lastname, Pageable pageable);
List<User> findByLastname(String lastname, Sort sort);
List<User> findByLastname(String lastname, Pageable pageable);
----
====
IMPORTANT: APIs taking `Sort` and `Pageable` expect non-`null` values to be handed into methods.
If you do not want to apply any sorting or pagination, use `Sort.unsorted()` and `Pageable.unpaged()`.
The first method lets you pass an `org.springframework.data.domain.Pageable` instance to the query method to dynamically add paging to your statically defined query.
A `Page` knows about the total number of elements and pages available.
It does so by the infrastructure triggering a count query to calculate the overall number.
As this might be expensive (depending on the store used), you can instead return a `Slice`.
A `Slice` knows only about whether a next `Slice` is available, which might be sufficient when walking through a larger result set.
Sorting options are handled through the `Pageable` instance, too.
If you need only sorting, add an `org.springframework.data.domain.Sort` parameter to your method.
As you can see, returning a `List` is also possible.
In this case, the additional metadata required to build the actual `Page` instance is not created (which, in turn, means that the additional count query that would have been necessary is not issued).
Rather, it restricts the query to look up only the given range of entities.
NOTE: To find out how many pages you get for an entire query, you have to trigger an additional count query.
By default, this query is derived from the query you actually trigger.
[[repositories.paging-and-sorting]]
==== Paging and Sorting
You can define simple sorting expressions by using property names.
You can concatenate expressions to collect multiple criteria into one expression.
.Defining sort expressions
====
[source,java]
----
Sort sort = Sort.by("firstname").ascending()
.and(Sort.by("lastname").descending());
----
====
For a more type-safe way to define sort expressions, start with the type for which to define the sort expression and use method references to define the properties on which to sort.
.Defining sort expressions by using the type-safe API
====
[source,java]
----
TypedSort<Person> person = Sort.sort(Person.class);
Sort sort = person.by(Person::getFirstname).ascending()
.and(person.by(Person::getLastname).descending());
----
====
NOTE: `TypedSort.by(…)` makes use of runtime proxies by (typically) using CGlib, which may interfere with native image compilation when using tools such as Graal VM Native.
If your store implementation supports Querydsl, you can also use the generated metamodel types to define sort expressions:
.Defining sort expressions by using the Querydsl API
====
[source,java]
----
QSort sort = QSort.by(QPerson.firstname.asc())
.and(QSort.by(QPerson.lastname.desc()));
----
====
[[repositories.limit-query-result]]
=== Limiting Query Results
You can limit the results of query methods by using the `first` or `top` keywords, which you can use interchangeably.
You can append an optional numeric value to `top` or `first` to specify the maximum result size to be returned.
If the number is left out, a result size of 1 is assumed.
The following example shows how to limit the query size:
.Limiting the result size of a query with `Top` and `First`
====
[source,java]
----
User findFirstByOrderByLastnameAsc();
User findTopByOrderByAgeDesc();
Page<User> queryFirst10ByLastname(String lastname, Pageable pageable);
Slice<User> findTop3ByLastname(String lastname, Pageable pageable);
List<User> findFirst10ByLastname(String lastname, Sort sort);
List<User> findTop10ByLastname(String lastname, Pageable pageable);
----
====
The limiting expressions also support the `Distinct` keyword for datastores that support distinct queries.
Also, for the queries that limit the result set to one instance, wrapping the result into with the `Optional` keyword is supported.
If pagination or slicing is applied to a limiting query pagination (and the calculation of the number of available pages), it is applied within the limited result.
NOTE: Limiting the results in combination with dynamic sorting by using a `Sort` parameter lets you express query methods for the 'K' smallest as well as for the 'K' biggest elements.
include::repositories-paging-sorting.adoc[]
[[repositories.collections-and-iterables]]
=== Repository Methods Returning Collections or Iterables

View File

@@ -0,0 +1,144 @@
/*
* 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.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* A {@link ScrollPosition} based on the last seen keyset. Keyset scrolling must be associated with a {@link Sort
* well-defined sort} to be able to extract the keyset when resuming scrolling within the sorted result set.
*
* @author Mark Paluch
* @since 3.1
*/
public final class KeysetScrollPosition implements ScrollPosition {
private static final KeysetScrollPosition initial = new KeysetScrollPosition(Collections.emptyMap(),
Direction.Forward);
private final Map<String, Object> keys;
private final Direction direction;
private KeysetScrollPosition(Map<String, Object> keys, Direction direction) {
this.keys = keys;
this.direction = direction;
}
/**
* Creates a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
*
* @return a new initial {@link KeysetScrollPosition} to start scrolling using keyset-queries.
*/
public static KeysetScrollPosition initial() {
return initial;
}
/**
* Creates a new {@link KeysetScrollPosition} from a keyset.
*
* @param keys must not be {@literal null}.
* @return a new {@link KeysetScrollPosition} for the given keyset.
*/
public static KeysetScrollPosition of(Map<String, ?> keys) {
return of(keys, Direction.Forward);
}
/**
* Creates a new {@link KeysetScrollPosition} from a keyset and {@link Direction}.
*
* @param keys must not be {@literal null}.
* @param direction must not be {@literal null}.
* @return a new {@link KeysetScrollPosition} for the given keyset and {@link Direction}.
*/
public static KeysetScrollPosition of(Map<String, ?> keys, Direction direction) {
Assert.notNull(keys, "Keys must not be null");
Assert.notNull(direction, "Direction must not be null");
if (keys.isEmpty()) {
return initial();
}
return new KeysetScrollPosition(Collections.unmodifiableMap(new LinkedHashMap<>(keys)), direction);
}
@Override
public boolean isInitial() {
return keys.isEmpty();
}
/**
* @return the keyset.
*/
public Map<String, Object> getKeys() {
return keys;
}
/**
* @return the scroll direction.
*/
public Direction getDirection() {
return direction;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
KeysetScrollPosition that = (KeysetScrollPosition) o;
return ObjectUtils.nullSafeEquals(keys, that.keys) && direction == that.direction;
}
@Override
public int hashCode() {
int result = 17;
result += 31 * ObjectUtils.nullSafeHashCode(keys);
result += 31 * ObjectUtils.nullSafeHashCode(direction);
return result;
}
@Override
public String toString() {
return String.format("KeysetScrollPosition [%s, %s]", direction, keys);
}
/**
* Keyset scrolling direction.
*/
enum Direction {
/**
* Forward (default) direction to scroll from the beginning of the results to their end.
*/
Forward,
/**
* Backward direction to scroll from the end of the results to their beginning.
*/
Backward;
}
}

View File

@@ -0,0 +1,127 @@
/*
* 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.function.IntFunction;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* A {@link ScrollPosition} based on the offsets within query results.
*
* @author Mark Paluch
* @since 3.1
*/
public final class OffsetScrollPosition implements ScrollPosition {
private static final OffsetScrollPosition initial = new OffsetScrollPosition(0);
private final long offset;
private OffsetScrollPosition(long offset) {
this.offset = offset;
}
/**
* Creates a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit.
*
* @return a new initial {@link OffsetScrollPosition} to start scrolling using offset/limit.
*/
public static OffsetScrollPosition initial() {
return initial;
}
/**
* Creates a new {@link OffsetScrollPosition} from an {@code offset}.
*
* @param offset
* @return a new {@link OffsetScrollPosition} with the given {@code offset}.
*/
public static OffsetScrollPosition of(long offset) {
if (offset == 0) {
return initial();
}
return new OffsetScrollPosition(offset);
}
/**
* Returns the {@link IntFunction position function} to calculate.
*
* @param startOffset the start offset to be used. Must not be negative.
* @return the offset-based position function.
*/
public static IntFunction<OffsetScrollPosition> positionFunction(long startOffset) {
Assert.isTrue(startOffset >= 0, "Start offset must not be negative");
return startOffset == 0 ? OffsetPositionFunction.ZERO : new OffsetPositionFunction(startOffset);
}
@Override
public boolean isInitial() {
return offset == 0;
}
/**
* @return the offset.
*/
public long getOffset() {
return offset;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
OffsetScrollPosition that = (OffsetScrollPosition) o;
return offset == that.offset;
}
@Override
public int hashCode() {
int result = 17;
result += 31 * ObjectUtils.nullSafeHashCode(offset);
return result;
}
@Override
public String toString() {
return String.format("OffsetScrollPosition [%s]", offset);
}
private record OffsetPositionFunction(long startOffset) implements IntFunction<OffsetScrollPosition> {
static final OffsetPositionFunction ZERO = new OffsetPositionFunction(0);
@Override
public OffsetScrollPosition apply(int offset) {
if (offset < 0) {
throw new IndexOutOfBoundsException(offset);
}
return of(startOffset + offset + 1);
}
}
}

View File

@@ -161,4 +161,20 @@ public interface Pageable {
return isUnpaged() ? Optional.empty() : Optional.of(this);
}
/**
* Returns an {@link OffsetScrollPosition} from this pageable if the page request {@link #isPaged() is paged}.
*
* @return
* @throws IllegalStateException if the request is {@link #isUnpaged()}
* @since 3.1
*/
default OffsetScrollPosition toScrollPosition() {
if (isUnpaged()) {
throw new IllegalStateException("Cannot create OffsetScrollPosition from an unpaged instance");
}
return OffsetScrollPosition.of(getOffset());
}
}

View File

@@ -0,0 +1,167 @@
/*
* 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.List;
import java.util.NoSuchElementException;
import java.util.function.Function;
import java.util.function.IntFunction;
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
* contains a subset of the actual query results for easier consumption of large result sets. The scroll is less
* opinionated about the actual data retrieval, whether the query has used index/offset, keyset-based pagination or
* cursor resume tokens.
*
* @author Mark Paluch
* @since 3.1
* @see ScrollPosition
*/
public interface Scroll<T> extends Streamable<T> {
/**
* Construct a {@link Scroll}.
*
* @param items the list of data.
* @param positionFunction the list of data.
* @return the {@link Scroll}.
* @param <T>
*/
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction) {
return new ScrollImpl<>(items, positionFunction, false);
}
/**
* Construct a {@link Scroll}.
*
* @param items the list of data.
* @param positionFunction the list of data.
* @param hasNext
* @return the {@link Scroll}.
* @param <T>
*/
static <T> Scroll<T> from(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
return new ScrollImpl<>(items, positionFunction, hasNext);
}
/**
* Returns the number of elements in this scroll.
*
* @return the number of elements in this scroll.
*/
int size();
/**
* Returns {@code true} if this scroll contains no elements.
*
* @return {@code true} if this scroll contains no elements
*/
boolean isEmpty();
/**
* Returns the scroll content as {@link List}.
*
* @return
*/
List<T> getContent();
/**
* Returns whether the current scroll is the last one.
*
* @return
*/
default boolean isLast() {
return !hasNext();
}
/**
* Returns if there is a next scroll.
*
* @return if there is a next scroll window.
*/
boolean hasNext();
/**
* Returns the {@link ScrollPosition} at {@code index}.
*
* @param index
* @return
* @throws IndexOutOfBoundsException if the index is out of range ({@code index < 0 || index >= size()}).
*/
ScrollPosition positionAt(int index);
/**
* Returns the {@link ScrollPosition} for {@code object}.
*
* @param object
* @return
* @throws NoSuchElementException if the object is not part of the result.
*/
default ScrollPosition positionAt(T object) {
int index = getContent().indexOf(object);
if (index == -1) {
throw new NoSuchElementException();
}
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.
*
* @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}.
* @return a new {@link Scroll} with the content of the current one mapped by the given {@code converter}.
*/
<U> Scroll<U> map(Function<? super T, ? extends U> converter);
}

View File

@@ -0,0 +1,119 @@
/*
* 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.Iterator;
import java.util.List;
import java.util.function.Function;
import java.util.function.IntFunction;
import java.util.stream.Collectors;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
/**
* Default {@link Scroll} implementation.
*
* @author Mark Paluch
* @since 3.1
*/
class ScrollImpl<T> implements Scroll<T> {
private final List<T> items;
private final IntFunction<? extends ScrollPosition> positionFunction;
private final boolean hasNext;
ScrollImpl(List<T> items, IntFunction<? extends ScrollPosition> positionFunction, boolean hasNext) {
Assert.notNull(items, "List of items must not be null");
Assert.notNull(positionFunction, "Position function must not be null");
this.items = items;
this.positionFunction = positionFunction;
this.hasNext = hasNext;
}
@Override
public int size() {
return items.size();
}
@Override
public boolean isEmpty() {
return items.isEmpty();
}
@Override
public List<T> getContent() {
return items;
}
@Override
public boolean hasNext() {
return hasNext;
}
@Override
public ScrollPosition positionAt(int index) {
if (index < 0 || index >= size()) {
throw new IndexOutOfBoundsException(index);
}
return positionFunction.apply(index);
}
@Override
public <U> Scroll<U> map(Function<? super T, ? extends U> converter) {
Assert.notNull(converter, "Function must not be null");
return new ScrollImpl<>(stream().map(converter).collect(Collectors.toList()), positionFunction, hasNext);
}
@NotNull
@Override
public Iterator<T> iterator() {
return items.iterator();
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ScrollImpl<?> that = (ScrollImpl<?>) o;
return ObjectUtils.nullSafeEquals(items, that.items)
&& ObjectUtils.nullSafeEquals(positionFunction, that.positionFunction)
&& ObjectUtils.nullSafeEquals(hasNext, that.hasNext);
}
@Override
public int hashCode() {
int result = ObjectUtils.nullSafeHashCode(items);
result = 31 * result + ObjectUtils.nullSafeHashCode(positionFunction);
result = 31 * result + ObjectUtils.nullSafeHashCode(hasNext);
return result;
}
@Override
public String toString() {
return "Scroll " + items;
}
}

View File

@@ -0,0 +1,33 @@
/*
* 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;
/**
* Interface to specify a position within a total query result. Scroll positions are used to start scrolling from the
* beginning of a query result or to resume scrolling from a given position within the query result.
*
* @author Mark Paluch
* @since 3.1
*/
public interface ScrollPosition {
/**
* Returns whether the current scroll position is the initial one.
*
* @return
*/
boolean isInitial();
}

View File

@@ -26,6 +26,8 @@ import java.util.stream.Stream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -44,10 +46,23 @@ public interface FluentQuery<T> {
* @param sort the {@link Sort} specification to sort the results by, may be {@link Sort#unsorted()}, must not be
* {@literal null}.
* @return a new instance of {@link FluentQuery}.
* @throws IllegalArgumentException if resultType is {@code null}.
* @throws IllegalArgumentException if {@code sort} is {@code null}.
*/
FluentQuery<T> sortBy(Sort sort);
/**
* Define the query limit.
*
* @param limit the limit to apply to the query to limit results. Must not be negative.
* @return a new instance of {@link FluentQuery}.
* @throws IllegalArgumentException if {@code limit} is less than zero.
* @throws UnsupportedOperationException if not supported by the underlying implementation.
* @since 3.1
*/
default FluentQuery<T> limit(int limit) {
throw new UnsupportedOperationException("Limit not supported");
}
/**
* Define the target type the result should be mapped to. Skip this step if you are only interested in the original
* domain type.
@@ -55,7 +70,7 @@ public interface FluentQuery<T> {
* @param resultType must not be {@code null}.
* @param <R> result type.
* @return a new instance of {@link FluentQuery}.
* @throws IllegalArgumentException if resultType is {@code null}.
* @throws IllegalArgumentException if {@code resultType} is {@code null}.
*/
<R> FluentQuery<R> as(Class<R> resultType);
@@ -64,7 +79,7 @@ public interface FluentQuery<T> {
*
* @param properties must not be {@code null}.
* @return a new instance of {@link FluentQuery}.
* @throws IllegalArgumentException if fields is {@code null}.
* @throws IllegalArgumentException if {@code properties} is {@code null}.
*/
default FluentQuery<T> project(String... properties) {
return project(Arrays.asList(properties));
@@ -75,7 +90,7 @@ public interface FluentQuery<T> {
*
* @param properties must not be {@code null}.
* @return a new instance of {@link FluentQuery}.
* @throws IllegalArgumentException if fields is {@code null}.
* @throws IllegalArgumentException if {@code properties} is {@code null}.
*/
FluentQuery<T> project(Collection<String> properties);
@@ -90,6 +105,11 @@ public interface FluentQuery<T> {
@Override
FetchableFluentQuery<T> sortBy(Sort sort);
@Override
default FetchableFluentQuery<T> limit(int limit) {
throw new UnsupportedOperationException("Limit not supported");
}
@Override
<R> FetchableFluentQuery<R> as(Class<R> resultType);
@@ -144,12 +164,27 @@ public interface FluentQuery<T> {
*/
List<T> all();
/**
* Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at
* {@code scrollPosition}.
*
* @param scrollPosition must not be {@literal null}.
* @return
* @throws IllegalArgumentException if {@code scrollPosition} is {@literal null}.
* @throws UnsupportedOperationException if not supported by the underlying implementation.
* @since 3.1
*/
default Scroll<T> scroll(ScrollPosition scrollPosition) {
throw new UnsupportedOperationException("Scrolling not supported");
}
/**
* Get a page of matching elements for {@link Pageable}.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if
* the {@link Sort} object is not {@link Sort#isUnsorted()}.
* the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)}
* will be overridden by {@link Pageable#getPageSize()}.
* @return
*/
Page<T> page(Pageable pageable);
@@ -187,6 +222,11 @@ public interface FluentQuery<T> {
@Override
ReactiveFluentQuery<T> sortBy(Sort sort);
@Override
default ReactiveFluentQuery<T> limit(int limit) {
throw new UnsupportedOperationException("Limit not supported");
}
@Override
<R> ReactiveFluentQuery<R> as(Class<R> resultType);
@@ -220,12 +260,27 @@ public interface FluentQuery<T> {
*/
Flux<T> all();
/**
* Get all matching elements as {@link Scroll} to start result scrolling or resume scrolling at
* {@code scrollPosition}.
*
* @param scrollPosition must not be {@literal null}.
* @return
* @throws IllegalArgumentException if {@code scrollPosition} is {@literal null}.
* @throws UnsupportedOperationException if not supported by the underlying implementation.
* @since 3.1
*/
default Mono<Scroll<T>> scroll(ScrollPosition scrollPosition) {
throw new UnsupportedOperationException("Scrolling not supported");
}
/**
* Get a page of matching elements for {@link Pageable}.
*
* @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
* {@literal null}. The given {@link Pageable} will override any previously specified {@link Sort sort} if
* the {@link Sort} object is not {@link Sort#isUnsorted()}.
* the {@link Sort} object is not {@link Sort#isUnsorted()}. Any potentially specified {@link #limit(int)}
* will be overridden by {@link Pageable#getPageSize()}.
* @return
*/
Mono<Page<T>> page(Pageable pageable);

View File

@@ -26,6 +26,7 @@ import java.util.Optional;
import org.springframework.core.MethodParameter;
import org.springframework.core.ResolvableType;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.util.ClassUtils;
import org.springframework.data.repository.util.QueryExecutionConverters;
@@ -57,7 +58,7 @@ public class Parameter {
static {
List<Class<?>> types = new ArrayList<>(Arrays.asList(Pageable.class, Sort.class));
List<Class<?>> types = new ArrayList<>(Arrays.asList(ScrollPosition.class, Pageable.class, Sort.class));
// consider Kotlin Coroutines Continuation a special parameter. That parameter is synthetic and should not get
// bound to any query.
@@ -192,6 +193,16 @@ public class Parameter {
return format("%s:%s", isNamedParameter() ? getName() : "#" + getIndex(), getType().getName());
}
/**
* Returns whether the {@link Parameter} is a {@link ScrollPosition} parameter.
*
* @return
* @since 3.1
*/
boolean isScrollPosition() {
return ScrollPosition.class.isAssignableFrom(getType());
}
/**
* Returns whether the {@link Parameter} is a {@link Pageable} parameter.
*

View File

@@ -18,6 +18,7 @@ package org.springframework.data.repository.query;
import java.util.Iterator;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.lang.Nullable;
@@ -29,6 +30,14 @@ import org.springframework.lang.Nullable;
*/
public interface ParameterAccessor extends Iterable<Object> {
/**
* Returns the {@link ScrollPosition} of the parameters, if available. Returns {@code null} otherwise.
*
* @return
*/
@Nullable
ScrollPosition getScrollPosition();
/**
* Returns the {@link Pageable} of the parameters, if available. Returns {@link Pageable#unpaged()} otherwise.
*

View File

@@ -28,6 +28,7 @@ import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.Streamable;
@@ -42,7 +43,7 @@ import org.springframework.util.Assert;
*/
public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter> implements Streamable<T> {
public static final List<Class<?>> TYPES = Arrays.asList(Pageable.class, Sort.class);
public static final List<Class<?>> TYPES = Arrays.asList(ScrollPosition.class, Pageable.class, Sort.class);
private static final String PARAM_ON_SPECIAL = format("You must not use @%s on a parameter typed %s or %s",
Param.class.getSimpleName(), Pageable.class.getSimpleName(), Sort.class.getSimpleName());
@@ -52,6 +53,7 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final int scrollPositionIndex;
private final int pageableIndex;
private final int sortIndex;
private final List<T> parameters;
@@ -91,6 +93,7 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
this.parameters = new ArrayList<>(parameterCount);
this.dynamicProjectionIndex = -1;
int scrollPositionIndex = -1;
int pageableIndex = -1;
int sortIndex = -1;
@@ -111,6 +114,10 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
this.dynamicProjectionIndex = parameter.getIndex();
}
if (ScrollPosition.class.isAssignableFrom(parameter.getType())) {
scrollPositionIndex = i;
}
if (Pageable.class.isAssignableFrom(parameter.getType())) {
pageableIndex = i;
}
@@ -122,6 +129,7 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
parameters.add(parameter);
}
this.scrollPositionIndex = scrollPositionIndex;
this.pageableIndex = pageableIndex;
this.sortIndex = sortIndex;
this.bindable = Lazy.of(this::getBindable);
@@ -138,6 +146,7 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
this.parameters = new ArrayList<>(originals.size());
int scrollPositionIndexTemp = -1;
int pageableIndexTemp = -1;
int sortIndexTemp = -1;
int dynamicProjectionTemp = -1;
@@ -147,11 +156,13 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
T original = originals.get(i);
this.parameters.add(original);
scrollPositionIndexTemp = original.isScrollPosition() ? i : -1;
pageableIndexTemp = original.isPageable() ? i : -1;
sortIndexTemp = original.isSort() ? i : -1;
dynamicProjectionTemp = original.isDynamicProjectionParameter() ? i : -1;
}
this.scrollPositionIndex = scrollPositionIndexTemp;
this.pageableIndex = pageableIndexTemp;
this.sortIndex = sortIndexTemp;
this.dynamicProjectionIndex = dynamicProjectionTemp;
@@ -185,6 +196,27 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
return (T) new Parameter(parameter);
}
/**
* Returns whether the method the {@link Parameters} was created for contains a {@link ScrollPosition} argument.
*
* @return
* @since 3.1
*/
public boolean hasScrollPositionParameter() {
return scrollPositionIndex != -1;
}
/**
* Returns the index of the {@link ScrollPosition} {@link Method} parameter if available. Will return {@literal -1} if
* there is no {@link ScrollPosition} argument in the {@link Method}'s parameter list.
*
* @return the scrollPositionIndex
* @since 3.1
*/
public int getScrollPositionIndex() {
return scrollPositionIndex;
}
/**
* Returns whether the method the {@link Parameters} was created for contains a {@link Pageable} argument.
*
@@ -195,8 +227,8 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
}
/**
* Returns the index of the {@link Pageable} {@link Method} parameter if available. Will return {@literal -1} if
* there is no {@link Pageable} argument in the {@link Method}'s parameter list.
* Returns the index of the {@link Pageable} {@link Method} parameter if available. Will return {@literal -1} if there
* is no {@link Pageable} argument in the {@link Method}'s parameter list.
*
* @return the pageableIndex
*/
@@ -205,8 +237,8 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
}
/**
* Returns the index of the {@link Sort} {@link Method} parameter if available. Will return {@literal -1} if there
* is no {@link Sort} argument in the {@link Method}'s parameter list.
* Returns the index of the {@link Sort} {@link Method} parameter if available. Will return {@literal -1} if there is
* no {@link Sort} argument in the {@link Method}'s parameter list.
*
* @return
*/
@@ -288,7 +320,7 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
* @return
*/
public boolean hasSpecialParameter() {
return hasSortParameter() || hasPageableParameter();
return hasScrollPositionParameter() || hasSortParameter() || hasPageableParameter();
}
/**
@@ -315,8 +347,8 @@ public abstract class Parameters<S extends Parameters<S, T>, T extends Parameter
/**
* Returns a bindable parameter with the given index. So for a method with a signature of
* {@code (Pageable pageable, String name)} a call to {@code #getBindableParameter(0)} will return the
* {@link String} parameter.
* {@code (Pageable pageable, String name)} a call to {@code #getBindableParameter(0)} will return the {@link String}
* parameter.
*
* @param bindableIndex
* @return

View File

@@ -18,6 +18,7 @@ package org.springframework.data.repository.query;
import java.util.Iterator;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
@@ -91,6 +92,22 @@ public class ParametersParameterAccessor implements ParameterAccessor {
return this.values;
}
@Override
public ScrollPosition getScrollPosition() {
if (!parameters.hasScrollPositionParameter()) {
Pageable pageable = getPageable();
if (pageable.isPaged()) {
return pageable.toScrollPosition();
}
return null;
}
return (ScrollPosition) values[parameters.getScrollPositionIndex()];
}
@Override
public Pageable getPageable() {

View File

@@ -18,11 +18,14 @@ package org.springframework.data.repository.query;
import static org.springframework.data.repository.util.ClassUtils.*;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Set;
import java.util.stream.Stream;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.domain.Sort;
import org.springframework.data.projection.ProjectionFactory;
@@ -31,6 +34,7 @@ import org.springframework.data.repository.core.RepositoryMetadata;
import org.springframework.data.repository.util.QueryExecutionConverters;
import org.springframework.data.repository.util.ReactiveWrapperConverters;
import org.springframework.data.util.Lazy;
import org.springframework.data.util.ReactiveWrappers;
import org.springframework.data.util.TypeInformation;
import org.springframework.util.Assert;
@@ -92,6 +96,10 @@ public class QueryMethod {
}
}
if (hasParameterOfType(method, ScrollPosition.class)) {
assertReturnTypeAssignable(method, Collections.singleton(Scroll.class));
}
Assert.notNull(this.parameters,
() -> String.format("Parameters extracted from method '%s' must not be null", method.getName()));
@@ -100,6 +108,12 @@ public class QueryMethod {
String.format("Paging query needs to have a Pageable parameter; Offending method: %s", method));
}
if (isScrollQuery()) {
Assert.isTrue(this.parameters.hasScrollPositionParameter() || this.parameters.hasPageableParameter(),
String.format("Scroll query needs to have a ScrollPosition parameter; Offending method: %s", method));
}
this.domainClass = Lazy.of(() -> {
Class<?> repositoryDomainClass = metadata.getDomainType();
@@ -188,6 +202,16 @@ public class QueryMethod {
return isCollectionQuery.get();
}
/**
* Returns whether the query method will return a {@link Scroll}.
*
* @return
* @since 3.1
*/
public boolean isScrollQuery() {
return org.springframework.util.ClassUtils.isAssignable(Scroll.class, unwrappedReturnType);
}
/**
* Returns whether the query method will return a {@link Slice}.
*
@@ -268,7 +292,7 @@ public class QueryMethod {
private boolean calculateIsCollectionQuery() {
if (isPageQuery() || isSliceQuery()) {
if (isPageQuery() || isSliceQuery() || isScrollQuery()) {
return false;
}
@@ -314,6 +338,10 @@ public class QueryMethod {
// TODO: to resolve generics fully we'd need the actual repository interface here
TypeInformation<?> returnType = TypeInformation.fromReturnTypeOf(method);
returnType = ReactiveWrappers.isSingleValueType(returnType.getType()) //
? returnType.getRequiredComponentType() //
: returnType;
returnType = QueryExecutionConverters.isSingleValue(returnType.getType()) //
? returnType.getRequiredComponentType() //
: returnType;
@@ -324,6 +352,6 @@ public class QueryMethod {
}
}
throw new IllegalStateException("Method has to have one of the following return types" + types);
throw new IllegalStateException("Method has to have one of the following return types " + types);
}
}

View File

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

View File

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

View File

@@ -223,7 +223,7 @@ public class CustomCollections {
/**
* Returns whether the current's raw type is one of the given ones.
*
* @param candidates must not be {@literal null}.
* @param type must not be {@literal null}.
* @return
*/
public boolean has(Class<?> type) {

View File

@@ -0,0 +1,43 @@
/*
* 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 java.util.Collections;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.KeysetScrollPosition.Direction;
/**
* Unit tests for {@link KeysetScrollPosition}.
*
* @author Mark Paluch
*/
class KeysetScrollPositionUnitTests {
@Test // GH-2151
void equalsAndHashCode() {
KeysetScrollPosition foo1 = KeysetScrollPosition.of(Collections.singletonMap("k", "v"));
KeysetScrollPosition foo2 = KeysetScrollPosition.of(Collections.singletonMap("k", "v"));
KeysetScrollPosition bar = KeysetScrollPosition.of(Collections.singletonMap("k", "v"), Direction.Backward);
assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
}
}

View File

@@ -0,0 +1,50 @@
/*
* 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.springframework.data.domain.OffsetScrollPosition.*;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link OffsetScrollPosition}.
*
* @author Mark Paluch
*/
class OffsetScrollPositionUnitTests {
@Test // GH-2151
void equalsAndHashCode() {
OffsetScrollPosition foo1 = OffsetScrollPosition.of(1);
OffsetScrollPosition foo2 = OffsetScrollPosition.of(1);
OffsetScrollPosition bar = OffsetScrollPosition.of(2);
assertThat(foo1).isEqualTo(foo2).hasSameClassAs(foo2);
assertThat(foo1).isNotEqualTo(bar).doesNotHaveSameHashCodeAs(bar);
}
@Test // GH-2151
void shouldCreateCorrectIndexPosition() {
assertThat(positionFunction(0).apply(0)).isEqualTo(OffsetScrollPosition.of(1));
assertThat(positionFunction(0).apply(1)).isEqualTo(OffsetScrollPosition.of(2));
assertThat(positionFunction(100).apply(0)).isEqualTo(OffsetScrollPosition.of(101));
assertThat(positionFunction(100).apply(1)).isEqualTo(OffsetScrollPosition.of(102));
}
}

View File

@@ -19,7 +19,6 @@ import static org.assertj.core.api.Assertions.*;
import static org.springframework.data.domain.UnitTestUtils.*;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Sort.Direction;
/**
@@ -67,4 +66,12 @@ class PageRequestUnitTests extends AbstractPageRequestUnitTests {
assertThatIllegalArgumentException() //
.isThrownBy(() -> PageRequest.of(0, 10, null));
}
@Test // GH-2151
void createsOffsetScrollPosition() {
PageRequest request = PageRequest.of(1, 10);
assertThat(request.toScrollPosition()).isEqualTo(OffsetScrollPosition.of(10));
}
}

View File

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

View File

@@ -19,14 +19,17 @@ import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
/**
* Unit tests for {@link ParametersParameterAccessor}.
*
* @author Oliver Gierke
* @author Greg Turnquist
* @author Mark Paluch
*/
class ParametersParameterAccessorUnitTests {
@@ -75,6 +78,18 @@ class ParametersParameterAccessorUnitTests {
assertThat(accessor.getBindableValue(0)).isEqualTo("Foo");
}
@Test // GH-2151
void handlesScrollPositionAsAParameterType() throws NoSuchMethodException {
var method = Sample.class.getMethod("method", ScrollPosition.class, String.class);
var parameters = new DefaultParameters(method);
var accessor = new ParametersParameterAccessor(parameters, new Object[] { OffsetScrollPosition.of(1), "Foo" });
assertThat(accessor).hasSize(1);
assertThat(accessor.getBindableValue(0)).isEqualTo("Foo");
}
@Test // #2626
void handlesPageRequestAsAParameterType() throws NoSuchMethodException {
@@ -93,6 +108,8 @@ class ParametersParameterAccessorUnitTests {
void method(Pageable pageable, String string);
void method(ScrollPosition scrollPosition, String string);
void methodWithPageRequest(PageRequest pageRequest, String string);
}
}

View File

@@ -25,9 +25,10 @@ import java.util.Optional;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.reactivestreams.Publisher;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.Sort;
import org.springframework.test.util.ReflectionTestUtils;
@@ -182,6 +183,14 @@ class ParametersUnitTests {
assertThat(parameters.hasPageableParameter()).isTrue();
}
@Test // GH-2151
void acceptsScrollPositionSubtypeParameter() throws Exception {
var parameters = getParametersFor("customScrollPosition", OffsetScrollPosition.class);
assertThat(parameters.hasScrollPositionParameter()).isTrue();
}
private Parameters<?, Parameter> getParametersFor(String methodName, Class<?>... parameterTypes)
throws SecurityException, NoSuchMethodException {
@@ -221,6 +230,8 @@ class ParametersUnitTests {
void methodWithSingle(Single<String> single);
Page<Object> customPageable(SomePageable pageable);
Scroll<Object> customScrollPosition(OffsetScrollPosition request);
}
interface SomePageable extends Pageable {}

View File

@@ -19,6 +19,7 @@ import static org.assertj.core.api.Assertions.*;
import io.vavr.collection.Seq;
import io.vavr.control.Option;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.io.Serializable;
@@ -31,6 +32,8 @@ import org.eclipse.collections.api.list.ImmutableList;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Scroll;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Slice;
import org.springframework.data.projection.ProjectionFactory;
import org.springframework.data.projection.SpelAwareProxyProjectionFactory;
@@ -88,6 +91,48 @@ class QueryMethodUnitTests {
assertThat(queryMethod.isCollectionQuery()).isFalse();
}
@Test // GH-2151
void supportsImperativecursorQueries() throws Exception {
var method = SampleRepository.class.getMethod("cursorWindow", ScrollPosition.class);
var queryMethod = new QueryMethod(method, metadata, factory);
assertThat(queryMethod.isPageQuery()).isFalse();
assertThat(queryMethod.isScrollQuery()).isTrue();
assertThat(queryMethod.isCollectionQuery()).isFalse();
}
@Test // GH-2151
void supportsReactiveCursorQueries() throws Exception {
var method = SampleRepository.class.getMethod("reactiveCursorWindow", ScrollPosition.class);
var queryMethod = new QueryMethod(method, metadata, factory);
assertThat(queryMethod.isPageQuery()).isFalse();
assertThat(queryMethod.isScrollQuery()).isTrue();
assertThat(queryMethod.isCollectionQuery()).isFalse();
}
@Test // GH-2151
void rejectsInvalidReactiveCursorQueries() throws Exception {
var method = SampleRepository.class.getMethod("invalidReactiveCursorWindow", ScrollPosition.class);
assertThatIllegalStateException().isThrownBy(() -> new QueryMethod(method, metadata, factory));
}
@Test // GH-2151
void rejectsCursorWindowMethodWithoutPageable() throws Exception {
var method = SampleRepository.class.getMethod("cursorWindowWithoutScrollPosition");
assertThatIllegalArgumentException().isThrownBy(() -> new QueryMethod(method, metadata, factory));
}
@Test // GH-2151
void rejectsCursorWindowMethodWithInvalidReturnType() throws Exception {
var method = SampleRepository.class.getMethod("cursorWindowMethodWithInvalidReturnType", ScrollPosition.class);
assertThatIllegalStateException().isThrownBy(() -> new QueryMethod(method, metadata, factory));
}
@Test // DATACMNS-171
void detectsAnEntityBeingReturned() throws Exception {
@@ -305,6 +350,16 @@ class QueryMethodUnitTests {
Mono<Slice<User>> reactiveSlice();
ImmutableList<User> returnsEclipseCollection();
Scroll<User> cursorWindow(ScrollPosition cursorRequest);
Mono<Scroll<User>> reactiveCursorWindow(ScrollPosition cursorRequest);
Flux<Scroll<User>> invalidReactiveCursorWindow(ScrollPosition cursorRequest);
Page<User> cursorWindowMethodWithInvalidReturnType(ScrollPosition cursorRequest);
Scroll<User> cursorWindowWithoutScrollPosition();
}
class User {

View File

@@ -19,23 +19,27 @@ import static org.assertj.core.api.Assertions.*;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.data.domain.OffsetScrollPosition;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.ScrollPosition;
import org.springframework.data.domain.Sort;
/**
* Unit tests for {@link ParametersParameterAccessor}.
*
* @author Oliver Gierke
* @author Mark Paluch
*/
class SimpleParameterAccessorUnitTests {
Parameters<?, ?> parameters, sortParameters, pageableParameters;
Parameters<?, ?> parameters, cursorRequestParameters, sortParameters, pageableParameters;
@BeforeEach
void setUp() throws SecurityException, NoSuchMethodException {
parameters = new DefaultParameters(Sample.class.getMethod("sample", String.class));
cursorRequestParameters = new DefaultParameters(Sample.class.getMethod("sample", ScrollPosition.class));
sortParameters = new DefaultParameters(Sample.class.getMethod("sample1", String.class, Sort.class));
pageableParameters = new DefaultParameters(Sample.class.getMethod("sample2", String.class, Pageable.class));
}
@@ -75,6 +79,16 @@ class SimpleParameterAccessorUnitTests {
assertThat(accessor.getSort().isSorted()).isFalse();
}
@Test // GH-2151
void returnsScrollPositionIfAvailable() {
var cursorRequest = OffsetScrollPosition.of(1);
ParameterAccessor accessor = new ParametersParameterAccessor(cursorRequestParameters,
new Object[] { cursorRequest });
assertThat(accessor.getScrollPosition()).isEqualTo(cursorRequest);
}
@Test
void returnsSortIfAvailable() {
@@ -110,6 +124,8 @@ class SimpleParameterAccessorUnitTests {
void sample(String firstname);
void sample(ScrollPosition scrollPosition);
void sample1(String firstname, Sort sort);
void sample2(String firstname, Pageable pageable);