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:
@@ -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 3→4 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).
|
||||
|
||||
Reference in New Issue
Block a user