Skip to content

Add support for keyset- and offset scrolling #2885

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 7 commits into from
Closed

Conversation

mp911de
Copy link
Member

@mp911de mp911de commented Mar 23, 2023

We now support offset- and keyset-scrolling through query derivation, Query-by-Example and Querydsl.

interface UserRepository extends Repository<User, Long> {
  Window<User> findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc(String firstname,
			ScrollPosition position);
}

Window<User> firstWindow = repository.findTop3ByFirstnameStartingWithOrderByFirstnameAscEmailAddressAsc("J",
				KeysetScrollPosition.initial());

String-based query methods are not supported yet because we do not rewrite the query. For HQL/JPQL, we might introduce this functionality later (see #2886). We do not intend to support keyset scrolling for stored procedures as we have no metadata over the stored procedure and its API.

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Mar 23, 2023
@mp911de mp911de linked an issue Mar 23, 2023 that may be closed by this pull request
Copy link
Contributor

@schauder schauder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't make it through all the changes.

Some of my comments are just documentation of my confusion. I'd need more time to clarify if those are due to my limited brain power or a problem in the code.
But I don't know when I will find time for that so, it is what it is.

@Test // GH-2878
void shouldReturnFirstItems() {

assertThat(CollectionUtils.getFirst(2, List.of(1, 2, 3))).hasSize(2).containsSequence(1, 2);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason to use .hasSize(2).containsSequence(1, 2) instead of .containsExactly(1,2)?
Equivalent for all other asserts in this class.

for (Order order : sort) {

if (!keysetValues.containsKey(order.getProperty())) {
throw new IllegalStateException("KeysetScrollPosition does not contain all keyset values");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have the missing property and the actual keyset in the error message.

List<P> or = new ArrayList<>();

int i = 0;
// progressive query building
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What the heck are we doing here? Honest question, I don't get it.

It looks like
if we order by A, B, C and have keyset values A-> a, B-> b, C-> c, D->d
we create the constraint
(A > a and B>b and C>c)
or (B>b and C>c)
or (C>c)

which should be equivalent to (C>c) and be completely useless 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is: A > a || A == a && B > b || A == a && B == b && C > c assuming ORDER BY A asc, B asc, C asc

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we have something to that effect in a comment in the code?

* @author Mark Paluch
* @since 3.1
*/
public class ScrollDelegate<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I have a problem with the name part Delegate. A delegate is something that takes care of part of a task. In that sense pretty much every class is a delegate. It makes sense in a certain role, i.e. as a field name where it becomes a hint that the Delegate pattern is used. But as part of a class name it conveys exactly zero information to me, maybe a little less.

Copy link
Contributor

@gregturn gregturn Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with Jens that this doesn't appear to be an implementation of the Delegate pattern so may need a more suitable name. I'd hate to say ScrollUtils as those tend to be collections of procedural static functions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've followed Spring Framework practices (see https://github.com/spring-projects/spring-framework/blob/18adf905a8eb33bfaaaa42d4d6d078027f8090ea/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java#L32) to create an object that implements a certain behavior that can be used from various places.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While most of the stuff that the Spring (Framework) team does is pretty awesome, in this case I'm not a fan.

I'd hate to say ScrollUtils as those tend to be collections of procedural static functions.

Or maybe just Scroller?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have the same delegation pattern in e.g. RepositoryConfigurationDelegate where we encapsulate a subset of functionality in an object that is being used as delegate from other places.


/**
* Adapter to construct scroll queries.
*
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really an "adapter" pattern? Adapters typically implement implement interfaces with empty methods.

@@ -63,7 +60,7 @@ public KeysetScrollSpecification(KeysetScrollPosition position, Sort sort, JpaEn
*/
public static Sort createSort(KeysetScrollPosition position, Sort sort, JpaEntityInformation<?, ?> entity) {

KeysetScrollDirector director = KeysetScrollDirector.of(position.getDirection());
KeysetScrollDelegate director = KeysetScrollDelegate.of(position.getDirection());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If KeysetScrollDirector is no more, then the variable name probably needs to change as well.

@@ -80,7 +79,7 @@ public Window<T> scroll(Query query, Sort sort, ScrollPosition scrollPosition) {
private static <T> Window<T> createWindow(Sort sort, int limit, Direction direction,
JpaEntityInformation<T, ?> entity, List<T> result) {

KeysetScrollDirector director = KeysetScrollDirector.of(direction);
KeysetScrollDelegate director = KeysetScrollDelegate.of(direction);
List<T> resultsToUse = director.postProcessResults(result);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See prior comment about var name.

if (scrollPosition instanceof KeysetScrollPosition keyset) {

KeysetScrollDelegate director = KeysetScrollDelegate.of(keyset.getDirection());
sort = KeysetScrollSpecification.createSort(keyset, sort, entityInformation);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See other comment about var name.

q -> q.limit(3).sortBy(Sort.by("firstname", "emailAddress")).scroll(backward));

assertThat(previousWindow).hasSize(2).containsSequence(jane1, jane2);

Copy link
Contributor

@gregturn gregturn Mar 23, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need .containsExactlyInThisOrder to better assert the order of data in this Window? I'm presuming that position 3 was john1, and you went backward to jane2 and then jane1, which I'd expect to see as .containsExactlyInThisOrder(jane2, jane1) as a reflection of this backward traversal.

Quite possibly, we should assert that way for the other Windows as well, to further highlight the ordering of data and traversal.


@Override
public FetchableFluentQuery<R> limit(int limit) {
Assert.isTrue(limit >= 0, "Limit must not be negative");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New line

@mp911de mp911de marked this pull request as ready for review March 24, 2023 08:40
@mp911de mp911de added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Mar 24, 2023
Copy link
Contributor

@schauder schauder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some more comments. Nothing serious.

* @author Mark Paluch
* @since 3.1
*/
public class ScrollDelegate<T> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While most of the stuff that the Spring (Framework) team does is pretty awesome, in this case I'm not a fan.

I'd hate to say ScrollUtils as those tend to be collections of procedural static functions.

Or maybe just Scroller?

<3> Valid `Sort` containing explicitly _unsafe_ `Order`.
<4> Valid `Sort` expression pointing to aliased function.
====

[[jpa.query-methods.scroll]]
=== Scrolling large Query Results
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Large with capital L

[[jpa.query-methods.scroll]]
=== Scrolling large Query Results

When working with large data sets, <<repositories.scrolling,scrolling>> can help to process those results efficiently without loading all results into memory.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this section is written rather abstract and would prefer something more concrete.
Maybe something along the lines of:

When working with large data sets you cannot or don't want to load all result into memory at once. Spring Data JPA offers keyset-based and offset-based scrolling.

For more information about paging see paging
Offset-based scrolling becomes inefficient when the offset is large because the database still has to load the data, sort it just to count of the elements ignored due to the offset. See link for more information about this.

Keyset-based scrolling is based on returning a Window<T>
which apart from the actual result offers ways to access the next and previous window.

add example

While keyset-based scrolling is more efficient it has it's limitations as well.
Especially there is no concept of a numbered page, only of a next and previous window.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I extracted several impulses into spring-projects/spring-data-commons#2804 so that we can include less store-specific details here and can link to the actual elaboration on our scrolling methods.

/**
* Return the first {@code count} items from the list.
*
* @param count
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't provide a description for @param and @return we should skip them

/**
* Return the last {@code count} items from the list.
*
* @param count
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See above

/**
* Create a {@link Sort} object to be used with the actual query.
*
* @param position
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add some parameter description

private final CriteriaBuilder cb;

public JpaQueryStrategy(From<?, ?> from, CriteriaBuilder cb) {
this.from = from;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line


@Override
public Predicate compare(Order order, Expression<Comparable> propertyExpression, Object o) {
return order.isAscending() ? cb.greaterThan(propertyExpression, (Comparable) o)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line


@Override
public Expression<Comparable> createExpression(String property) {
PropertyPath path = PropertyPath.from(property, from.getJavaType());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blank line

@Override
public Map<String, Object> getKeyset(Iterable<String> propertyPaths, T entity) {

// TODO: proxy business?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we can't resolve the TODO now we should leave clearer instructions what exactly the TODO is, and potentially create a ticket for it.

@mp911de
Copy link
Member Author

mp911de commented Mar 29, 2023

Review comments addressed. I created a few tickets out of this review and incorporated documentation bits in Spring Data Commons so we need less store-specific documentation.

schauder pushed a commit that referenced this pull request Mar 29, 2023
schauder added a commit that referenced this pull request Mar 29, 2023
See #2878
Original pull request #2885
@schauder
Copy link
Contributor

That's merged.

@schauder schauder closed this Mar 29, 2023
@schauder schauder deleted the issue/2878 branch March 29, 2023 11:57
@schauder schauder added this to the 3.1 RC1 (2023.0.0) milestone Mar 29, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for Scroll API
5 participants