-
Notifications
You must be signed in to change notification settings - Fork 1.5k
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
Conversation
There was a problem hiding this 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); |
There was a problem hiding this comment.
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.
...src/main/java/org/springframework/data/jpa/repository/query/JpaKeysetScrollQueryCreator.java
Show resolved
Hide resolved
for (Order order : sort) { | ||
|
||
if (!keysetValues.containsKey(order.getProperty())) { | ||
throw new IllegalStateException("KeysetScrollPosition does not contain all keyset values"); |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 🤔
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
...ta-jpa/src/main/java/org/springframework/data/jpa/repository/query/KeysetScrollDelegate.java
Show resolved
Hide resolved
* @author Mark Paluch | ||
* @since 3.1 | ||
*/ | ||
public class ScrollDelegate<T> { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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. | ||
* |
There was a problem hiding this comment.
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()); | |||
|
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); | ||
|
There was a problem hiding this comment.
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"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New line
There was a problem hiding this 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> { |
There was a problem hiding this comment.
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
?
src/main/asciidoc/jpa.adoc
Outdated
<3> Valid `Sort` containing explicitly _unsafe_ `Order`. | ||
<4> Valid `Sort` expression pointing to aliased function. | ||
==== | ||
|
||
[[jpa.query-methods.scroll]] | ||
=== Scrolling large Query Results |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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()); |
There was a problem hiding this comment.
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? |
There was a problem hiding this comment.
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.
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. |
Closes #2878 Original pull request #2885 See spring-projects/spring-data-commons#2151
That's merged. |
We now support offset- and keyset-scrolling through query derivation, Query-by-Example and Querydsl.
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.