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:
2026-09-17 19:09:35 +00:00
parent 585eed4d57
commit 448c383879
41 changed files with 1811 additions and 17 deletions
+39 -14
View File
@@ -2,16 +2,17 @@
Companion code for the Spring Data JPA and transaction articles on **[ankurm.com](https://ankurm.com)**.
Two independent Maven modules under one aggregator:
Three independent Maven modules under one aggregator:
| Module | Article(s) | Boot / JDK |
|---|---|---|
| [`migration-behavior/`](migration-behavior) | the four Spring Data JPA 3→4 migration articles | 4.0.6 / 21 |
| [`migration-behavior/`](migration-behavior) | the four Spring Data JPA 34 migration articles | 4.0.6 / 21 |
| [`transactions/`](transactions) | [@Transactional: propagation, isolation and the six silent failures](https://ankurm.com/) | 4.1.1 / 25 |
| [`jdbc-vs-jpa/`](jdbc-vs-jpa) | [Spring Data JDBC vs Spring Data JPA in 2026: When Dropping the ORM Is the Right Call](https://ankurm.com/) | 4.1.1 / 25 |
The modules deliberately pin different Spring Boot versions. `migration-behavior` stays on
4.0.6 because that is what the four published migration articles were written and verified
against, and upgrading it would silently invalidate output those articles quote.
The modules deliberately pin different Spring Boot versions. `migration-behavior` stays on 4.0.6
because that is what the four published migration articles were written and verified against,
and upgrading it would silently invalidate output those articles quote.
> **Moved in September 2026.** The migration project used to live at the repository root. It is
> now under `migration-behavior/`; source paths gained that prefix and nothing else changed. The
@@ -19,20 +20,44 @@ against, and upgrading it would silently invalidate output those articles quote.
> original layout, so a link into a tagged tree is unaffected.
```bash
./mvnw -DskipTests package # both modules
./mvnw test # every test in both
./mvnw -DskipTests package # all modules
./mvnw test # every test in all three
```
## Tags
- `article-1-baseline`, `article-2-query-engine`, `article-3-advanced` - the exact code each article quotes, frozen at publish time.
- `corner-scenarios` (and `main`) - the enriched, current state described below. Some method signatures have moved on from the article-tagged snapshots (e.g. `Book`'s `price` is now an embedded `Money` value object, not a bare `BigDecimal`), so check out the matching article tag if you want the code to line up exactly with what's quoted in a given post.
- `article-1-baseline`, `article-2-query-engine`, `article-3-advanced` - the exact code each
migration article quotes, frozen at publish time.
- `corner-scenarios` (and `main`) - the enriched, current state described below. Some method
signatures have moved on from the article-tagged snapshots (e.g. `Book`'s `price` is now an
embedded `Money` value object, not a bare `BigDecimal`), so check out the matching article tag
if you want the code to line up exactly with what's quoted in a given post.
## What's covered beyond the three articles (corner-scenario enrichment)
- **Refined Specification API** (`AuthorSpecifications.java`): `PredicateSpecification` reused across a read and a bulk delete, an explicit `DeleteSpecification` (`CriteriaDelete`-backed), and an `UpdateSpecification` (`CriteriaUpdate`-backed bulk update composed from an `UpdateOperation` + a `where(...)` predicate).
- **`JpaSort.unsafe(...)` with a `CASE` expression** - a real `ORDER BY case when country = 'US' then 0 else 1 end` sort combined with a plain derived query.
- **`Money`, an `@Embeddable` record value object** on `Book.price`, with derived queries that traverse the embedded path (`findByPriceAmountGreaterThanEqual`, `findByPriceAmount`).
- **A genuine corner case, found by actually running it**: a derived-query class-based (record) projection resolves constructor-parameter names against *direct* entity properties only. `BookSummary(String title, BigDecimal amount)` does **not** resolve `amount` against the nested `price.amount` path via a plain `findBy...` derived method - it throws `PropertyReferenceException: No property 'amount' found for type 'Book'`. The fix is an explicit `@Query` constructor expression (`select new ...BookSummary(b.title, b.price.amount) from Book b where ...`), which does work for nested/embedded paths. See `BookRepository.findByPriceAmountLessThanEqual`.
- **Refined Specification API** (`AuthorSpecifications.java`): `PredicateSpecification` reused
across a read and a bulk delete, an explicit `DeleteSpecification` (`CriteriaDelete`-backed),
and an `UpdateSpecification` (`CriteriaUpdate`-backed bulk update composed from an
`UpdateOperation` + a `where(...)` predicate).
- **`JpaSort.unsafe(...)` with a `CASE` expression** - a real
`ORDER BY case when country = 'US' then 0 else 1 end` sort combined with a plain derived query.
- **`Money`, an `@Embeddable` record** value object on `Book.price`, with derived queries that
traverse the embedded path (`findByPriceAmountGreaterThanEqual`, `findByPriceAmount`).
- **A genuine corner case, found by actually running it**: a derived-query class-based (record)
projection resolves constructor-parameter names against *direct* entity properties only.
`BookSummary(String title, BigDecimal amount)` does **not** resolve `amount` against the nested
`price.amount` path via a plain `findBy...` derived method - it throws
`PropertyReferenceException: No property 'amount' found for type 'Book'`. The fix is an
explicit `@Query` constructor expression
(`select new ...BookSummary(b.title, b.price.amount) from Book b where ...`), which does handle
nested/embedded paths. See `BookRepository.findByPriceAmountLessThanEqual`.
All of the above is exercised by both the demo runner (`DemoRunner.java`, sections H-L) and dedicated tests in `MigrationBehaviorTests.java`.
All of the above is exercised by both the demo runner (`DemoRunner.java`, sections H-L) and
dedicated tests in `MigrationBehaviorTests.java`.
## jdbc-vs-jpa
Same `Customer`/`Order`/`OrderItem` domain modelled twice - Hibernate/Spring Data JPA and Spring
Data JDBC - against the same H2 database through the same statement-logging `DataSource`, so the
SQL-statement counts quoted in the article are a fair, apples-to-apples comparison. See
[`jdbc-vs-jpa/README.md`](jdbc-vs-jpa/README.md).