Skip to content

Commit eb64561

Browse files
authored
Add scroll API. (#94) (#95)
1 parent 267d82d commit eb64561

97 files changed

Lines changed: 3456 additions & 6026 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ Storm provides a Bill of Materials (BOM) for centralized version management. Imp
144144
<dependency>
145145
<groupId>st.orm</groupId>
146146
<artifactId>storm-bom</artifactId>
147-
<version>1.10.0</version>
147+
<version>1.11.0</version>
148148
<type>pom</type>
149149
<scope>import</scope>
150150
</dependency>
@@ -156,7 +156,7 @@ Storm provides a Bill of Materials (BOM) for centralized version management. Imp
156156

157157
```kotlin
158158
dependencies {
159-
implementation(platform("st.orm:storm-bom:1.10.0"))
159+
implementation(platform("st.orm:storm-bom:1.11.0"))
160160
}
161161
```
162162

@@ -166,7 +166,7 @@ With the BOM imported, add Storm modules without specifying versions:
166166

167167
```kotlin
168168
dependencies {
169-
implementation(platform("st.orm:storm-bom:1.10.0"))
169+
implementation(platform("st.orm:storm-bom:1.11.0"))
170170
implementation("st.orm:storm-kotlin")
171171
runtimeOnly("st.orm:storm-core")
172172
// Use storm-compiler-plugin-2.0 for Kotlin 2.0.x, -2.1 for 2.1.x, etc.

docs/api-java.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ The main Java API module. It provides the `ORMTemplate` entry point, repository
1616
<dependency>
1717
<groupId>st.orm</groupId>
1818
<artifactId>storm-java21</artifactId>
19-
<version>1.10.0</version>
19+
<version>1.11.0</version>
2020
</dependency>
2121
```
2222

@@ -44,7 +44,7 @@ Spring Framework integration for Java. Provides `RepositoryBeanFactoryPostProces
4444
<dependency>
4545
<groupId>st.orm</groupId>
4646
<artifactId>storm-spring</artifactId>
47-
<version>1.10.0</version>
47+
<version>1.11.0</version>
4848
</dependency>
4949
```
5050

@@ -58,7 +58,7 @@ Spring Boot auto-configuration for Java. Automatically creates an `ORMTemplate`
5858
<dependency>
5959
<groupId>st.orm</groupId>
6060
<artifactId>storm-spring-boot-starter</artifactId>
61-
<version>1.10.0</version>
61+
<version>1.11.0</version>
6262
</dependency>
6363
```
6464

@@ -83,7 +83,7 @@ The `storm-metamodel-processor` annotation processor generates type-safe metamod
8383
<dependency>
8484
<groupId>st.orm</groupId>
8585
<artifactId>storm-metamodel-processor</artifactId>
86-
<version>1.10.0</version>
86+
<version>1.11.0</version>
8787
<scope>provided</scope>
8888
</dependency>
8989
```

docs/api-kotlin.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ The main Kotlin API module. It provides the `ORMTemplate` interface, extension f
1414

1515
```kotlin
1616
// Gradle (Kotlin DSL)
17-
implementation("st.orm:storm-kotlin:1.10.0")
17+
implementation("st.orm:storm-kotlin:1.11.0")
1818
```
1919

2020
```xml
2121
<!-- Maven -->
2222
<dependency>
2323
<groupId>st.orm</groupId>
2424
<artifactId>storm-kotlin</artifactId>
25-
<version>1.10.0</version>
25+
<version>1.11.0</version>
2626
</dependency>
2727
```
2828

@@ -33,7 +33,7 @@ The Kotlin API does not depend on any preview features. All APIs are stable and
3333
Spring Framework integration for Kotlin. Provides `RepositoryBeanFactoryPostProcessor` for repository auto-discovery and injection, `@EnableTransactionIntegration` for bridging Storm's programmatic transactions with Spring's `@Transactional`, and transaction-aware coroutine support. Add this module when you use Spring Framework without Spring Boot.
3434

3535
```kotlin
36-
implementation("st.orm:storm-kotlin-spring:1.10.0")
36+
implementation("st.orm:storm-kotlin-spring:1.11.0")
3737
```
3838

3939
See [Spring Integration](spring-integration.md) for configuration details.
@@ -43,7 +43,7 @@ See [Spring Integration](spring-integration.md) for configuration details.
4343
Spring Boot auto-configuration for Kotlin. Automatically creates an `ORMTemplate` bean from the `DataSource`, discovers repositories, enables transaction integration, and binds `storm.*` properties from `application.yml`. This is the recommended dependency for Spring Boot applications.
4444

4545
```kotlin
46-
implementation("st.orm:storm-kotlin-spring-boot-starter:1.10.0")
46+
implementation("st.orm:storm-kotlin-spring-boot-starter:1.11.0")
4747
```
4848

4949
See [Spring Integration: Spring Boot Starter](spring-integration.md#spring-boot-starter) for what the starter provides and how to override its defaults.
@@ -96,7 +96,7 @@ plugins {
9696
}
9797

9898
dependencies {
99-
ksp("st.orm:storm-metamodel-ksp:1.10.0")
99+
ksp("st.orm:storm-metamodel-ksp:1.11.0")
100100
}
101101
```
102102

@@ -115,7 +115,7 @@ dependencies {
115115
<path>
116116
<groupId>st.orm</groupId>
117117
<artifactId>storm-metamodel-processor</artifactId>
118-
<version>1.10.0</version>
118+
<version>1.11.0</version>
119119
</path>
120120
</annotationProcessorPaths>
121121
</configuration>

docs/common-patterns.md

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -357,9 +357,9 @@ public class SoftDeleteGuard implements EntityCallback<Customer> {
357357

358358
---
359359

360-
## Pagination
360+
## Pagination and Scrolling
361361

362-
Storm provides built-in types for both offset-based and keyset-based pagination, so you do not need to define your own page wrappers or write raw `LIMIT`/`OFFSET` queries.
362+
Storm provides two strategies for traversing large result sets: pagination (by page number) and scrolling (by cursor). You do not need to define your own page wrappers or write raw `LIMIT`/`OFFSET` queries.
363363

364364
### Offset-Based Pagination
365365

@@ -443,56 +443,67 @@ Page<Ref<User>> refPage = userRepository.pageRef(0, 20);
443443
</TabItem>
444444
</Tabs>
445445

446-
### Keyset-Based Pagination
446+
### Scrolling
447447

448-
Use the `slice()`, `sliceAfter()`, and `sliceBefore()` methods on any entity repository. These use a unique column value (typically the primary key) as a cursor, which lets the database seek directly to the correct position using an index.
448+
Use the `scroll()` method on any entity repository with a `Scrollable` that captures the cursor state. These navigate sequentially using a unique column value (typically the primary key) as a cursor, which lets the database seek directly to the correct position using an index.
449449

450450
<Tabs groupId="language">
451451
<TabItem value="kotlin" label="Kotlin" default>
452452

453453
```kotlin
454454
// First page of 20 users ordered by ID
455-
val page1: Slice<User> = userRepository.slice(User_.id, 20)
455+
val window: Window<User> = userRepository.scroll(Scrollable.of(User_.id, 20))
456456

457-
// Next page using the last ID as cursor
458-
val page2: Slice<User> = userRepository.sliceAfter(User_.id, page1.content.last().id, 20)
457+
// Next page
458+
if (window.hasNext()) {
459+
val next: Window<User> = userRepository.scroll(window.nextScrollable())
460+
}
459461

460-
// Previous page before a known cursor
461-
val previous: Slice<User> = userRepository.sliceBefore(User_.id, someId, 20)
462+
// Previous page
463+
if (window.hasPrevious()) {
464+
val previous: Window<User> = userRepository.scroll(window.previousScrollable())
465+
}
462466
```
463467

464468
</TabItem>
465469
<TabItem value="java" label="Java">
466470

467471
```java
468472
// First page of 20 users ordered by ID
469-
Slice<User> page1 = userRepository.slice(User_.id, 20);
473+
Window<User> window = userRepository.scroll(Scrollable.of(User_.id, 20));
470474

471-
// Next page using the last ID as cursor
472-
User last = page1.content().getLast();
473-
Slice<User> page2 = userRepository.sliceAfter(User_.id, last.id(), 20);
475+
// Next page
476+
if (window.hasNext()) {
477+
Window<User> next = userRepository.scroll(window.nextScrollable());
478+
}
474479

475-
// Previous page before a known cursor
476-
Slice<User> previous = userRepository.sliceBefore(User_.id, someId, 20);
480+
// Previous page
481+
if (window.hasPrevious()) {
482+
Window<User> previous = userRepository.scroll(window.previousScrollable());
483+
}
477484
```
478485

479486
</TabItem>
480487
</Tabs>
481488

482-
Each method returns a `Slice` containing the page content and a `hasNext` flag. See [Repositories: Keyset Pagination](repositories.md#keyset-pagination) for the full API, including sort overloads, filtering, and ref variants.
489+
Each method returns a `Window` containing the page content, a `hasNext` flag, and navigation tokens (`nextScrollable()`, `previousScrollable()`) for sequential traversal. For REST APIs, `Window` also provides `nextCursor()` and `previousCursor()` to serialize the scroll position as an opaque string, and `Scrollable.fromCursor(key, cursor)` to reconstruct a `Scrollable` from a cursor string. See [Repositories: Scrolling](repositories.md#scrolling) for the full API, including sort overloads, filtering, and Ref variants.
483490

484491
### Choosing Between the Two
485492

486-
| Factor | Offset-Based (`page`) | Keyset-Based (`slice`) |
493+
| Factor | Pagination (`page`) | Scrolling (`scroll`) |
487494
|---|---|---|
488-
| Implementation complexity | Simple | Moderate |
489-
| Jump to arbitrary page | Yes | No (sequential only) |
495+
| Request type | `Pageable` | `Scrollable` |
496+
| Result type | `Page` | `Window` |
497+
| Navigation | page number | cursor |
498+
| Count query | yes | no |
499+
| Random access | yes | no |
490500
| Performance at page 1 | Good | Good |
491501
| Performance at page 1,000 | Degrades (database must skip rows) | Consistent (index seek) |
492502
| Handles concurrent inserts | Rows may shift between pages | Stable cursor |
493-
| Total element count | Included in `Page` | Not available in `Slice` |
503+
| Navigate forward | `page.nextPageable()` | `window.nextScrollable()` |
504+
| Navigate backward | `page.previousPageable()` | `window.previousScrollable()` |
494505

495-
Use offset-based pagination when you need random page access or a total count (for example, displaying "Page 3 of 12" in a UI). Use keyset pagination when you need consistent performance over deep result sets or when the data changes frequently between requests.
506+
Use pagination when you need random page access or a total count (for example, displaying "Page 3 of 12" in a UI). Use scrolling when you need consistent performance over deep result sets or when the data changes frequently between requests.
496507

497508
---
498509

docs/comparison.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ The following tables provide a side-by-side comparison of concrete features acro
3434
| SQL Templates | Yes | No | No | XML/Ann | Yes | Yes | No | No |
3535
| N+1 prevention | Yes | No | No | No | Manual | Manual | No | No |
3636
| Lazy loading | Refs | Yes | Yes | No | No | No | Yes | Yes |
37-
| Keyset pagination | Yes | No | Yes | No | Yes | No | No | No |
37+
| Scrolling | Yes | No | Yes | No | Yes | No | No | No |
3838
| JSON columns | Yes | Yes<sup>4</sup> | Via JPA | Manual | Yes | Module | Yes | Module |
3939
| JSON aggregation | Yes | No | No | No | Yes | No | No | No |
4040

docs/configuration.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Storm can be configured through `StormConfig`, system properties, or Spring Boot
2020
| `storm.validation.schema_mode` | `none` | Schema validation mode: `none`, `warn`, or `fail` (Spring Boot only) |
2121
| `storm.validation.strict` | `false` | Treat schema validation warnings as errors |
2222
| `storm.validation.interpolation_mode` | `warn` | Interpolation safety mode: `warn`, `fail`, or `none` (see [Interpolation Safety](#interpolation-safety)) |
23+
| `st.orm.scrollable.maxSize` | `1000` | Maximum window size allowed in a serialized cursor (system property only) |
2324

2425
### Setting Properties
2526

@@ -490,6 +491,22 @@ record Order(@PK Integer id,
490491

491492
---
492493

494+
## Scrolling Properties
495+
496+
### st.orm.scrollable.maxSize
497+
498+
Sets the maximum window size that a deserialized cursor (via `Scrollable.fromCursor()`) is allowed to carry. This is a safety limit that prevents untrusted clients from requesting excessively large pages through cursor manipulation. The limit is only enforced when deserializing a cursor string; programmatic usage via `Scrollable.of()` is not restricted.
499+
500+
This property is a JVM system property only; it is not configurable through `StormConfig` or `application.yml`, because it applies at the `Scrollable` record level in storm-foundation, before any ORM template is created.
501+
502+
```bash
503+
java -Dst.orm.scrollable.maxSize=5000 -jar myapp.jar
504+
```
505+
506+
Repository or API layers may choose to enforce stricter per-endpoint limits on top of this framework-level bound.
507+
508+
---
509+
493510
## Dirty Checking Properties
494511

495512
These properties control how Storm detects changes to entities during update operations. Dirty checking determines whether an UPDATE statement is sent to the database and, if so, which columns it includes. Choosing the right mode depends on your entity size, update frequency, and whether you use immutable records or mutable objects. See [Dirty Checking](dirty-checking.md) for a detailed explanation of each strategy.

0 commit comments

Comments
 (0)