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,38 @@
|
||||
# 05 — The aggregate boundary
|
||||
|
||||
[← 04 the JDBC side](04-the-jdbc-side.md) · [next: when this gets expensive →](06-when-this-gets-expensive.md)
|
||||
|
||||
Source: [`jdbc/Order.java`](../src/main/java/com/ankurm/jdbcvsjpa/jdbc/Order.java) (the
|
||||
`AggregateReference<Customer, Long> customer` field). Test:
|
||||
[`JdbcBehaviorTest#c_aggregateReferenceDoesNotLoadTheCustomer`](../src/test/java/com/ankurm/jdbcvsjpa/JdbcBehaviorTest.java),
|
||||
transcript [08](output/08-aggregate-reference-no-join.txt).
|
||||
|
||||
Domain-Driven Design's aggregate pattern says: an aggregate is a cluster of objects saved and
|
||||
loaded as one transactional unit, and a reference to *another* aggregate is held by identity, not
|
||||
by object graph. Spring Data JDBC enforces this at the type level. `Order.customer` is not a
|
||||
`Customer` field — it is an `AggregateReference<Customer, Long>`, a typed wrapper around a
|
||||
foreign-key value. `AggregateReference.getId()` returns that value without touching the database,
|
||||
because the value is already sitting in a column that was read as part of loading the order.
|
||||
|
||||
[Scenario 08](output/08-aggregate-reference-no-join.txt) demonstrates the consequence directly:
|
||||
loading an order and then reading `order.getCustomer().getId()` produces **zero** statements
|
||||
touching the customer table. There is no join, no lazy proxy, no N+1 waiting to happen — because
|
||||
there is no mechanism in Spring Data JDBC that would ever load a second aggregate as a side
|
||||
effect of loading the first one. If you want the `Customer`, you ask a `CustomerRepository` for
|
||||
it, explicitly, and that is a second, deliberate query.
|
||||
|
||||
This is the mechanism that makes [scenario 06](04-the-jdbc-side.md#findbyid-always-returns-the-whole-aggregate)'s
|
||||
"fixed cost regardless of item count" claim scale past one aggregate: the fixed cost is fixed
|
||||
*per aggregate*, and nothing about loading `Order` #1 ever cascades into loading `Order` #2's
|
||||
`Customer`, `Order` #2's `Customer`'s other orders, and so on. JPA's `@ManyToOne(fetch = LAZY)`
|
||||
gives you a proxy that resolves this the moment something touches it — convenient until the thing
|
||||
touching it is a `toString()` in a log statement three services away from the code that loaded
|
||||
the entity.
|
||||
|
||||
The trade is that Spring Data JDBC will not silently fetch related data across an aggregate
|
||||
boundary for you, ever, under any circumstance. If your domain genuinely needs a graph fetch
|
||||
(here's every order together with its customer's other orders), you write that query yourself.
|
||||
Some teams experience that as "more code to write." Others experience the JPA alternative as "a
|
||||
query plan I can no longer predict by reading the entity class."
|
||||
|
||||
Continue to [06 — when this gets expensive](06-when-this-gets-expensive.md).
|
||||
Reference in New Issue
Block a user