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.
39 lines
2.7 KiB
Markdown
39 lines
2.7 KiB
Markdown
# 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).
|