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.
48 lines
3.1 KiB
Markdown
48 lines
3.1 KiB
Markdown
# 02 — The JPA side
|
|
|
|
[← 01 the shared domain](01-the-shared-domain.md) · [next: the SQL log →](03-the-sql-log.md)
|
|
|
|
Source: [`jpa/Order.java`](../src/main/java/com/ankurm/jdbcvsjpa/jpa/Order.java),
|
|
[`jpa/OrderItem.java`](../src/main/java/com/ankurm/jdbcvsjpa/jpa/OrderItem.java),
|
|
[`jpa/Customer.java`](../src/main/java/com/ankurm/jdbcvsjpa/jpa/Customer.java). Tests:
|
|
[`JpaBehaviorTest.java`](../src/test/java/com/ankurm/jdbcvsjpa/JpaBehaviorTest.java).
|
|
|
|
## The five things demonstrated here
|
|
|
|
1. **Lazy collections need a live session.** [Scenario 01](output/01-lazy-outside-session.txt)
|
|
loads an `Order` inside 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: false` in
|
|
`application.yml` is what makes the session close at the transaction boundary instead of
|
|
silently staying open for the rest of the request; open-in-view defaults to `true` in plain
|
|
Spring Boot and papers over exactly this failure until it happens somewhere without a
|
|
surrounding web request.
|
|
2. **The default fetch shape is N+1.** [Scenario 02](output/02-n-plus-one.txt): `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.
|
|
3. **`@EntityGraph` flattens it to a constant.** [Scenario 03](output/03-entity-graph-fixed-cost.txt)
|
|
re-runs the same lookup through `findWithItemsAndCustomerById`, an `@EntityGraph` query, 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](05-the-aggregate-boundary.md).
|
|
4. **Dirty checking writes before you call save().**
|
|
[Scenario 04](output/04-dirty-checking-autoflush.txt): inside one transaction, mutate a
|
|
managed item's `quantity` field directly — no `save()` 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.
|
|
5. **`orphanRemoval` is what turns "removed from the list" into a DELETE.**
|
|
[Scenario 05](output/05-orphan-removal-delete.txt): `order.removeItem(item)` followed by
|
|
`save()` deletes that row because `orphanRemoval = true` is set on the `@OneToMany`. Drop that
|
|
attribute and the row survives with a stale `order_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](03-the-sql-log.md).
|