# jdbc-vs-jpa Companion code for **[Spring Data JDBC vs Spring Data JPA in 2026: When Dropping the ORM Is the Right Call](https://ankurm.com/)** on [ankurm.com](https://ankurm.com). The same `Customer` → `Order` → `OrderItem` domain, modelled twice — once with Hibernate/Spring Data JPA (`com.ankurm.jdbcvsjpa.jpa`), once with Spring Data JDBC (`com.ankurm.jdbcvsjpa.jdbc`) — running against the **same H2 database** through the **same** [`StatementLoggingDataSource`](src/main/java/com/ankurm/jdbcvsjpa/support/StatementLoggingDataSource.java), so every SQL-statement count quoted in the article is a fair, apples-to-apples measurement, not two separate benchmarks run under different conditions. **Tested with:** Spring Boot 4.1.1 / Spring Framework 7.0.9 / Spring Data JDBC 4.1.1 / Spring Data JPA (Hibernate ORM) via the Boot 4.1.1 BOM / H2 2.x / JDK 25 (Temurin 25.0.4.1). ## Quickstart ```bash ./mvnw test # runs everything, regenerates docs/output/ ./mvnw spring-boot:run # then: curl localhost:8080/diag/sql-log ``` ## Where things are | | | |---|---| | Shared domain | [docs/01-the-shared-domain.md](docs/01-the-shared-domain.md) | | JPA side | [docs/02-the-jpa-side.md](docs/02-the-jpa-side.md) — `src/main/java/.../jpa/` | | The SQL log mechanism | [docs/03-the-sql-log.md](docs/03-the-sql-log.md) — `src/main/java/.../support/` | | JDBC side | [docs/04-the-jdbc-side.md](docs/04-the-jdbc-side.md) — `src/main/java/.../jdbc/` | | The aggregate boundary | [docs/05-the-aggregate-boundary.md](docs/05-the-aggregate-boundary.md) | | Production checklist | [docs/06-when-this-gets-expensive.md](docs/06-when-this-gets-expensive.md) | ## Captured output Every number quoted in the article is one of these files, regenerated by `./mvnw test`: | File | What it shows | |---|---| | [docs/output/00-statement-count-comparison.txt](docs/output/00-statement-count-comparison.txt) | The headline table: statement counts, JPA vs JDBC, across order/item counts | | [docs/output/01-lazy-outside-session.txt](docs/output/01-lazy-outside-session.txt) | `LazyInitializationException` triggered for real | | [docs/output/02-n-plus-one.txt](docs/output/02-n-plus-one.txt) | N+1 from a plain `findAll()` + loop | | [docs/output/03-entity-graph-fixed-cost.txt](docs/output/03-entity-graph-fixed-cost.txt) | `@EntityGraph` flattening the cost to a constant | | [docs/output/04-dirty-checking-autoflush.txt](docs/output/04-dirty-checking-autoflush.txt) | An UPDATE with no `save()` call anywhere | | [docs/output/05-orphan-removal-delete.txt](docs/output/05-orphan-removal-delete.txt) | `orphanRemoval` turning a list removal into a DELETE | | [docs/output/06-fixed-cost-load.txt](docs/output/06-fixed-cost-load.txt) | Spring Data JDBC's fixed-statement-count `findById` | | [docs/output/07-delete-then-insert.txt](docs/output/07-delete-then-insert.txt) | The delete-then-batched-insert collection replace | | [docs/output/08-aggregate-reference-no-join.txt](docs/output/08-aggregate-reference-no-join.txt) | `AggregateReference` costing zero extra statements | | [docs/output/09-optimistic-locking.txt](docs/output/09-optimistic-locking.txt) | `@Version` conflict on both stacks | | [docs/output/10-list-order-key-column.txt](docs/output/10-list-order-key-column.txt) | `keyColumn` making list order deterministic | ## Diagnostic endpoint `GET /diag/sql-log` dumps every statement executed in this JVM since the last reset — the same mechanism the tests use. Delete `DiagController` before shipping; it is a demo aid, not a feature.