Files
sdjpa4-demo/jdbc-vs-jpa/docs/04-the-jdbc-side.md
T
asmhatre 448c383879 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.
2026-09-17 19:09:35 +00:00

3.1 KiB

04 — The JDBC side

← 03 the SQL log · next: the aggregate boundary →

Source: jdbc/Order.java, jdbc/OrderItem.java, jdbc/Customer.java. Tests: JdbcBehaviorTest.java.

findById always returns the whole aggregate

Scenario 06: findById on a 1-item order and on a 5-item order both take exactly 2 statements (one for the order row, one for its items, via a join-free two-query load — see the actual SQL in the transcript). There is no lazy/eager choice to make because there is no lazy loading. This is not an optimization Spring Data JDBC performs; it is the only thing it knows how to do, because an aggregate is, by definition, loaded and saved as one unit.

Saving replaces the whole collection — but as one batch

Scenario 07: changing the quantity on exactly one of three OrderItems and saving the aggregate produces a DELETE ... WHERE order_id = ? for all three existing rows and then a single batched INSERT (three rows, one JDBC batch execution — see 03 for how that's verified). Spring Data JDBC does not diff collections; it does not need to know which row changed, because it never tries to compute a diff. The cost is real (every row's data is retransmitted and reinserted, not just the one that changed) but it is one delete and one batch insert, not N+1 round trips. For a collection in the tens of rows this is invisible. For a collection in the tens of thousands, see 06 — when this gets expensive.

keyColumn is what makes list order survive a reload

Scenario 10: items saved in the order [SKU-Z, SKU-A, SKU-M] come back in that same order after a reload, because @MappedCollection(idColumn = "ORDER_ID", keyColumn = "ORDER_KEY") adds an extra integer column that records list position. Omit keyColumn and Spring Data JDBC still stores a List correctly — it just does not guarantee you the order back. On a fresh H2 table you will likely get insertion order anyway, which is exactly the kind of accident that stops being true the day someone runs a VACUUM, or the day the database changes.

Optimistic locking works the same way it does under JPA

Scenario 09: two callers load the same version, the first saves (version increments), the second — still holding the stale version — tries to save and gets OptimisticLockingFailureException. @Version is the same annotation (org.springframework.data.annotation.Version) doing conceptually the same thing: the UPDATE's WHERE clause includes the expected version, zero rows match a stale one, and Spring Data JDBC turns "zero rows updated" into an exception rather than silently doing nothing.

Continue to 05 — the aggregate boundary.