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
3.1 KiB
02 — The JPA side
← 01 the shared domain · next: the SQL log →
Source: jpa/Order.java,
jpa/OrderItem.java,
jpa/Customer.java. Tests:
JpaBehaviorTest.java.
The five things demonstrated here
- Lazy collections need a live session. Scenario 01
loads an
Orderinside a transaction, returns it, and touches.getItems()after that transaction (and the Hibernate session it owned) has closed —LazyInitializationException: ... (no session).spring.jpa.open-in-view: falseinapplication.ymlis what makes the session close at the transaction boundary instead of silently staying open for the rest of the request; open-in-view defaults totruein plain Spring Boot and papers over exactly this failure until it happens somewhere without a surrounding web request. - The default fetch shape is N+1. Scenario 02:
findAll()for 3 orders, then.getItems().size()on each in a loop, produces 1 + 3 = 4 SELECTs. Nothing about the code looks wrong; it is the ordinary shape of a service method. @EntityGraphflattens it to a constant. Scenario 03 re-runs the same lookup throughfindWithItemsAndCustomerById, an@EntityGraphquery, for a 1-item and a 5-item order — both take exactly 1 statement. This is the fix for scenario 2, and it is also the default (no annotation needed) on the JDBC side; see 05 — the aggregate boundary.- Dirty checking writes before you call save().
Scenario 04: inside one transaction, mutate a
managed item's
quantityfield directly — nosave()call anywhere — then run an unrelated query. The pending UPDATE appears before that second query runs, because Hibernate flushes dirty state ahead of anything that could otherwise see stale data. "I never called save()" is not evidence that nothing was written. orphanRemovalis what turns "removed from the list" into a DELETE. Scenario 05:order.removeItem(item)followed bysave()deletes that row becauseorphanRemoval = trueis set on the@OneToMany. Drop that attribute and the row survives with a staleorder_id— the more common real bug report.
What most tutorials don't show
Every one of the five behaviours above is correct — this is not a list of JPA bugs. The
argument this article makes is narrower: each of these five facts has to be known in advance and
opted into (an @EntityGraph, an orphanRemoval = true, an awareness of the flush timing) for
the obvious code to do the obvious thing. Spring Data JDBC, covered next, gets three of these five
for free by not having the mechanism that causes them.
Continue to 03 — the SQL log.