# 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).