Skip to content

feat: add easy-paging-reactive-demo (WebFlux + R2DBC against real PG)#7

Merged
jlc488 merged 3 commits into
mainfrom
feat/easy-paging-reactive-demo
May 23, 2026
Merged

feat: add easy-paging-reactive-demo (WebFlux + R2DBC against real PG)#7
jlc488 merged 3 commits into
mainfrom
feat/easy-paging-reactive-demo

Conversation

@jlc488
Copy link
Copy Markdown
Contributor

@jlc488 jlc488 commented May 23, 2026

Summary

Fourth and final easy-paging demo. Showcases the reactive companion artifact (`kr.devslab:easy-paging-spring-boot-starter-reactive:0.4.0`) — WebFlux + Spring Data R2DBC against real PostgreSQL via Testcontainers — so the same JSON pagination envelope as the MVC/MyBatis demos is now served as `Mono<PageResponse>` over a non-blocking stack.

Reuses the Docker pattern established in #5: `docker-compose.yml` for `bootRun`, Testcontainers + `@ServiceConnection` for `./gradlew test`. The compose file binds host port 5433 (not 5432) so the reactive and postgres demos can run side by side.

Architectural difference worth flagging

The MVC demos use the `@AutoPaginate` aspect, which is MyBatis/PageHelper-specific. On the reactive path there's no per-thread context to hook into, so the reactive starter instead exposes an explicit helper that the service calls directly:

```java
R2dbcOffsetPagingSupport.paginate(template, Article.class, criteria, pageable)
// → Mono<PageResponse

>
```

Wire-level contract is unchanged — clients can't tell which stack is behind a given endpoint.

Demo content

  • `Article` entity: `(id, title, author, publishedAt, viewCount)` — newsfeed use case, distinct from the postgres demo's `Product` so readers see entity mapping for a different domain.
  • Spring Data Relational mapping: `@Table` on the class, `@Column` only on fields where camelCase ≠ snake_case (`publishedAt`, `viewCount`).
  • Seed: 500 articles via `generate_series(1, 500)`, distributed deterministically across 5 authors (100 each), `view_count` deterministic so tests can assert exact values.
  • Service: one-liner around `R2dbcOffsetPagingSupport.paginate` with an optional `Criteria.where("author")` filter mirroring the postgres demo's `?category=`.

Integration test (`ArticleControllerIT`)

Uses WebTestClient (WebFlux) instead of MockMvc, and asserts via JSON paths — same shape as the postgres demo's tests with the reactive client swap. Covers:

  • Envelope metadata on unfiltered listing (`totalElements=500`, `totalPages=50`, `first=true`, `last=false`)
  • `?author=` filter narrowing `totalElements` from 500 to 100
  • `?sort=id,asc` returning Article chore: repurpose as Spring Boot starter examples repo #1 as `content[0]`
  • Last page reports `last=true` correctly

Verification

  • Compile: green locally.
  • Local test run: blocked by a Windows + Docker Desktop quirk (Testcontainers ends up talking to the CLI proxy npipe (`com.docker.desktop.address=npipe://...docker_cli`) and gets a 400 placeholder). I cleaned up the stale `~/.testcontainers.properties` from the previous setup, but a separate Docker Desktop named-pipe routing issue remains. CI on Ubuntu (unix socket, no proxies) is the meaningful verification — that will run on this PR.

CI expectation

The `detect` job should identify `easy-paging-reactive-demo/` as the only changed demo. The `build` job will then:

  1. Pull `postgres:16-alpine` (cached from feat: add easy-paging-postgres-demo (real PG via Docker + Testcontainers) #5).
  2. Start an ephemeral PG via Testcontainers.
  3. `@ServiceConnection` auto-wires the R2DBC `ConnectionFactory`.
  4. Spring Boot applies `schema.sql` + `data.sql` against the real PG.
  5. Run all 4 `ArticleControllerIT` tests.

Expected to take roughly the same as the postgres demo's CI (~50s) since the image is cached.

Closes the easy-paging roadmap

With this merged, all four originally-planned easy-paging scenarios have their own runnable demos:

Scenario Demo
Offset (basic) `easy-paging-demo` (H2)
Cursor / keyset `easy-paging-keyset-demo` (H2)
Production DB `easy-paging-postgres-demo` (Docker PG + Testcontainers)
Reactive `easy-paging-reactive-demo` (WebFlux + R2DBC + Docker PG + Testcontainers) — this PR

Test plan

  • CI `detect` job identifies only `easy-paging-reactive-demo` as changed
  • CI `build` job runs all 4 `ArticleControllerIT` tests against ephemeral Postgres R2DBC and turns green
  • After merge, top-level README lists all four easy-paging demos with working links

Note on PR #6

I noticed PR #6 (`ssrf-guard-demos`) is open in parallel. This PR is entirely orthogonal — different starter, different directories — and doesn't touch any files in that PR's diff. The top-level README row for the reactive demo is added between the postgres row and where the ssrf-guard rows will land; the two PRs will merge cleanly in either order.

Fourth easy-paging demo, completing the original starter coverage plan.
Showcases the reactive companion artifact
(kr.devslab:easy-paging-spring-boot-starter-reactive:0.4.0) — WebFlux +
Spring Data R2DBC against real PostgreSQL via Testcontainers — so the
same JSON pagination envelope as the MVC/MyBatis demos is served as
Mono<PageResponse<T>> over a non-blocking stack.

Reuses the Docker pattern established in easy-paging-postgres-demo:
docker-compose.yml for `bootRun`, Testcontainers + @Serviceconnection
for `./gradlew test`. The compose file binds host port 5433 (not 5432)
so the reactive and postgres demos can run side by side.

Architectural difference worth knowing about (also flagged in the
demo's README): the MVC demos use the @AutoPaginate aspect, which is
MyBatis/PageHelper-specific. On the reactive path there's no
per-thread context to hook into, so the reactive starter instead
exposes an explicit helper (R2dbcOffsetPagingSupport.paginate) the
service calls directly. Wire-level contract is unchanged.

Demo content:
- Article entity (id, title, author, publishedAt, viewCount) — newsfeed
  use case, distinct from the postgres demo's Product so readers can
  see the entity mapping for a different domain.
- 500 rows seeded via generate_series, deterministically across 5
  authors (100 each), so the integration test can assert exact totals.
- Service is a one-liner around R2dbcOffsetPagingSupport.paginate, with
  an optional Criteria.where("author") filter to mirror the postgres
  demo's ?category=.
- Spring Data Relational entity mapping: @table on the class, @column
  only on fields where camelCase ≠ snake_case (publishedAt, viewCount).

The integration test (ArticleControllerIT) covers:
- envelope metadata on unfiltered listing,
- ?author= filter narrowing totalElements from 500 to 100,
- sort=id,asc returning Article #1 as content[0],
- last page reporting last=true correctly.

Uses WebTestClient (WebFlux) instead of MockMvc (servlet) and asserts
via JSON paths — same shape as the postgres demo's tests with the
reactive client swap.

Top-level README updated to list this demo after the postgres one and
before the ssrf-guard rows.

Local verification note: my Windows + Docker Desktop setup has the
named-pipe quirk where Testcontainers ends up talking to the CLI
proxy pipe (com.docker.desktop.address=npipe://...docker_cli) and
getting a 400 placeholder response, so the local `./gradlew test`
couldn't reach a real Docker engine. Compile passes cleanly, and CI
on a clean Ubuntu runner (unix socket, no proxies) is the meaningful
verification — that will run on this PR.
@jlc488 jlc488 force-pushed the feat/easy-paging-reactive-demo branch from 8683ea0 to c990df3 Compare May 23, 2026 05:53
jlc488 added 2 commits May 23, 2026 14:56
…+ Sort

First CI run on PR #7 failed all four ArticleControllerIT tests with
500 INTERNAL_SERVER_ERROR, and the verbose-logging commit surfaced the
real cause:

    java.lang.IllegalStateException: No primary or single unique
      constructor found for interface org.springframework.data.domain.Pageable

Spring Boot's SpringDataWebAutoConfiguration only registers
PageableHandlerMethodArgumentResolver for Spring MVC; the
@EnableSpringDataWebSupport annotation is also servlet-only.
WebFlux apps that want `Pageable pageable` method parameters have to
wire ReactivePageableHandlerMethodArgumentResolver (and the matching
Sort one) themselves via a WebFluxConfigurer bean.

PagingWebFluxConfig now does exactly that. The Javadoc on the class
documents the failure mode so the next person reading the file sees
straight away why this seemingly redundant boilerplate exists.

Trap worth documenting (and the kind of thing future reactive demos
in this repo will need to repeat — the postgres demo, which uses
servlet MVC, doesn't hit this because Spring Boot auto-wires the
servlet resolver for it).
@jlc488 jlc488 merged commit 01d2fc9 into main May 23, 2026
2 checks passed
@jlc488 jlc488 deleted the feat/easy-paging-reactive-demo branch May 23, 2026 06:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant