committed by
Christoph Strobl
parent
a5de842460
commit
035965a3a2
@@ -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.
|
||||
|
||||
136
src/main/asciidoc/repositories-paging-sorting.adoc
Normal file
136
src/main/asciidoc/repositories-paging-sorting.adoc
Normal 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.
|
||||
83
src/main/asciidoc/repositories-scrolling.adoc
Normal file
83
src/main/asciidoc/repositories-scrolling.adoc
Normal 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.
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
167
src/main/java/org/springframework/data/domain/Scroll.java
Normal file
167
src/main/java/org/springframework/data/domain/Scroll.java
Normal 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);
|
||||
|
||||
}
|
||||
119
src/main/java/org/springframework/data/domain/ScrollImpl.java
Normal file
119
src/main/java/org/springframework/data/domain/ScrollImpl.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user