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.
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.