Add jdbc-vs-jpa module: Spring Data JDBC vs JPA on a shared Order aggregate
Same Customer/Order/OrderItem domain modelled with Hibernate/Spring Data JPA and Spring Data JDBC side by side, both running through a shared StatementLoggingDataSource so SQL-statement counts are directly comparable. 11 tests, 11 captured transcripts, 6 doc chapters. Companion repo for the ankurm.com article on when to drop the ORM.
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
# 06 — When this gets expensive (production checklist)
|
||||
|
||||
[← 05 the aggregate boundary](05-the-aggregate-boundary.md)
|
||||
|
||||
A short, honest checklist for deciding between the two stacks on a real project, expanding on the
|
||||
article's closing "should you even do this" callout.
|
||||
|
||||
## Reach for Spring Data JDBC when
|
||||
|
||||
- Your aggregates are genuinely small (single digits to low tens of child rows). The
|
||||
delete-and-reinsert cost in [07](output/07-delete-then-insert.txt) is proportional to
|
||||
aggregate size on every save, not to what changed.
|
||||
- You have been bitten by `LazyInitializationException`, N+1 queries you didn't predict, or a
|
||||
flush timing surprise (scenarios [01](output/01-lazy-outside-session.txt),
|
||||
[02](output/02-n-plus-one.txt), [04](output/04-dirty-checking-autoflush.txt)) more than once on
|
||||
the same codebase.
|
||||
- You want the query plan for "load this thing" to be readable from the entity class alone, with
|
||||
no session state, no `@EntityGraph` annotations to remember, and no proxy that decides at
|
||||
runtime whether to hit the database.
|
||||
- You are comfortable writing more explicit queries (`@Query`, `JdbcAggregateTemplate`) for
|
||||
anything that crosses an aggregate boundary, because that boundary is enforced rather than
|
||||
advisory.
|
||||
|
||||
## Stay on Spring Data JPA when
|
||||
|
||||
- Collections are large or unbounded (hundreds to thousands of child rows) and updated
|
||||
incrementally — the full delete-and-reinsert becomes real, measurable write amplification.
|
||||
- Your domain genuinely benefits from a persistence-context-managed object graph: complex
|
||||
bidirectional relationships, inheritance hierarchies, or second-level caching.
|
||||
- Your team already has deep Hibernate operational experience (query plan reading, N+1 detection
|
||||
tooling, `@BatchSize` tuning) and the tooling investment already exists.
|
||||
- You need JPQL/Criteria API's richer query composition, or a library that specifically expects
|
||||
`EntityManager`.
|
||||
|
||||
## What migrating does NOT require
|
||||
|
||||
You do not have to choose one stack for a whole application. This module runs both in one Spring
|
||||
context (`@EnableJpaRepositories` scoped to one package, `@EnableJdbcRepositories` scoped to
|
||||
another — see [`JdbcVsJpaApplication.java`](../src/main/java/com/ankurm/jdbcvsjpa/JdbcVsJpaApplication.java)).
|
||||
A pragmatic migration moves the aggregates that are small, hot, and bug-prone under JPA's
|
||||
lazy-loading surprises first, and leaves the genuinely graph-shaped, cache-heavy parts of the
|
||||
domain on JPA.
|
||||
|
||||
## What to measure before deciding, on your own schema
|
||||
|
||||
1. p95 aggregate size (row count) for the collections you're considering moving.
|
||||
2. Write frequency versus read frequency for those aggregates — JDBC's replace-on-save cost is a
|
||||
write-path cost; if reads dominate, it may not matter.
|
||||
3. Whether any code today relies on partial, incremental updates to a large collection
|
||||
(`@Query` bulk updates exist on both sides, but the ORM-managed dirty-checking path some teams
|
||||
lean on for this does not exist under Spring Data JDBC at all).
|
||||
|
||||
[← back to the shared domain](01-the-shared-domain.md) · [README](../README.md)
|
||||
Reference in New Issue
Block a user