English · 한국어
Production-flavoured runnable example for easy-paging-spring-boot-starter against a real PostgreSQL instead of H2.
The starter works on whatever JDBC database PageHelper supports (Postgres, MySQL, MariaDB, Oracle, …) — this demo proves it end-to-end on the database most teams actually ship with, and shows the Docker Compose + Testcontainers pattern this repo will use for every "external DB" demo from here on.
- JDK 21+
- Docker (Docker Desktop or any Docker-compatible runtime)
- That's it. No local Postgres install. No psql client needed.
Each demo is a standalone Gradle project, so you can grab this one folder without
cloning the whole devslab-examples repo.
With git (sparse checkout):
git clone --filter=blob:none --sparse https://github.com/devslab-kr/devslab-examples.git
cd devslab-examples
git sparse-checkout set easy-paging-postgres-demo
cd easy-paging-postgres-demoWithout git (folder only):
curl -sL https://github.com/devslab-kr/devslab-examples/archive/refs/heads/main.tar.gz \
| tar -xz --strip-components=2 devslab-examples-main/easy-paging-postgres-demo
cd easy-paging-postgres-democd easy-paging-postgres-demo
# Start PostgreSQL in the background. The compose file binds 5432 on localhost.
docker compose up -d db
# Boot the app. spring.sql.init recreates the schema and seeds 500 products
# on every start, so you always boot into a known state.
./gradlew bootRunThe app comes up on http://localhost:8080. When you're done:
docker compose down # stop the container
docker compose down -v # ...and drop the volume (clean slate next time)# Default offset pagination
curl 'http://localhost:8080/products?page=0&size=10'
# Sort
curl 'http://localhost:8080/products?page=0&size=10&sort=price,desc'
curl 'http://localhost:8080/products?page=0&size=10&sort=createdAt,desc&sort=id,asc'
# Filter by category — data is seeded into 5 categories, 100 each
curl 'http://localhost:8080/products?category=books&page=0&size=20' | jq '.totalElements'
# → 100
# Page-size clamping (controller declares @AutoPaginate(maxSize = 100))
curl 'http://localhost:8080/products?size=9999' | jq '.size, (.content | length)'
# → 100
# → 100
# Sort-injection attempts get HTTP 400 before they ever reach the database
curl -i 'http://localhost:8080/products?sort=name;DROP%20TABLE%20products'There are two Docker paths in this demo, deliberately separate:
| Path | When it runs | What it uses |
|---|---|---|
docker compose up -d db |
When a human wants to bootRun against a long-lived DB |
docker-compose.yml in this directory — published port 5432, named volume pgdata |
Testcontainers in ProductControllerIT |
When ./gradlew test runs (locally or in CI) |
An ephemeral postgres:16-alpine container on a random port, started + torn down per test class |
The integration test uses Spring Boot 3.1+'s @ServiceConnection, which auto-rewires spring.datasource.url to the Testcontainers instance — no application-test.yml, no @DynamicPropertySource. The compose file's port and the test's port don't conflict because the test never reads docker-compose.yml.
This means CI can run ./gradlew build on a clean Ubuntu runner with no Postgres pre-installed and the integration tests still hit a real Postgres. The runner already has Docker; Testcontainers handles the rest.
The interesting parts, in order:
| File | Why |
|---|---|
docker-compose.yml |
minimal PG service with a healthcheck, so docker compose up -d is a one-liner |
build.gradle.kts |
adds org.postgresql:postgresql (driver) and the spring-boot-testcontainers / org.testcontainers:postgresql test deps; everything else is identical to the H2 demo |
src/test/.../ProductControllerIT.java |
@Testcontainers + @ServiceConnection is all the wiring needed; no static initializer block, no @DynamicPropertySource |
src/main/resources/schema.sql |
uses BIGSERIAL and a composite index — PG-native, not H2-portable — to show the starter is happy with real PG schema features |
src/main/resources/data.sql |
generate_series(1, 500) for deterministic seeding (no random() for fields tests assert against) |
product/ProductController.java |
the contract is one annotation + Pageable — same as the H2 demo. The starter doesn't care which database is underneath |
This demo uses spring.sql.init with a DROP TABLE IF EXISTS + CREATE TABLE script that runs on every boot. Don't do that in production. Use Flyway or Liquibase — both have Spring Boot starters and both coexist with easy-paging-spring-boot-starter without any special configuration. spring.sql.init is fine for a learning demo where the goal is "always boot into a known state with no setup steps".
./gradlew buildThis runs ProductControllerIT against an ephemeral Testcontainers Postgres. First run pulls the postgres:16-alpine image (~80MB); subsequent runs use the cached image and complete in seconds.
easy-paging-demo |
easy-paging-postgres-demo (this) |
|
|---|---|---|
| Database | H2 in-memory (no external runtime) | Real PostgreSQL (Docker Compose for bootRun, Testcontainers for tests) |
| Schema features used | basic table | BIGSERIAL, NUMERIC(10,2), composite index, generate_series |
| Mapper logic | always SELECT * |
optional WHERE category = ? via <if> |
| Test infra | none beyond MockMvc | Testcontainers + @ServiceConnection |
| Best for | "show me the starter in 30 seconds" | "show me it works against a production DB and that tests can integrate cleanly" |