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,50 @@
|
||||
# 04 — The JDBC side
|
||||
|
||||
[← 03 the SQL log](03-the-sql-log.md) · [next: the aggregate boundary →](05-the-aggregate-boundary.md)
|
||||
|
||||
Source: [`jdbc/Order.java`](../src/main/java/com/ankurm/jdbcvsjpa/jdbc/Order.java),
|
||||
[`jdbc/OrderItem.java`](../src/main/java/com/ankurm/jdbcvsjpa/jdbc/OrderItem.java),
|
||||
[`jdbc/Customer.java`](../src/main/java/com/ankurm/jdbcvsjpa/jdbc/Customer.java). Tests:
|
||||
[`JdbcBehaviorTest.java`](../src/test/java/com/ankurm/jdbcvsjpa/JdbcBehaviorTest.java).
|
||||
|
||||
## findById always returns the whole aggregate
|
||||
|
||||
[Scenario 06](output/06-fixed-cost-load.txt): `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](output/07-delete-then-insert.txt): changing the quantity on exactly one of three
|
||||
`OrderItem`s 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](03-the-sql-log.md) 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](06-when-this-gets-expensive.md).
|
||||
|
||||
## `keyColumn` is what makes list order survive a reload
|
||||
|
||||
[Scenario 10](output/10-list-order-key-column.txt): 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](output/09-optimistic-locking.txt): 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](05-the-aggregate-boundary.md).
|
||||
Reference in New Issue
Block a user