Files
sdjpa4-demo/jdbc-vs-jpa/docs/01-the-shared-domain.md
T
asmhatre 448c383879 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.
2026-09-17 19:09:35 +00:00

2.7 KiB

01 — The shared domain

← README · next: the JPA side →

Both stacks in this module model the same thing: a Customer who places Orders, each with a list of OrderItems. It is deliberately the smallest domain that has everything worth arguing about — a one-to-many collection, a reference to a second aggregate, and a field a reader will want to mutate in place.

Concept JPA package (com.ankurm.jdbcvsjpa.jpa) JDBC package (com.ankurm.jdbcvsjpa.jdbc)
Table prefix jpa_* JDBC_* (see the identifier-casing note below)
Order → Customer @ManyToOne(fetch = LAZY) object reference AggregateReference<Customer, Long> — a typed foreign key, never a loaded object
Order → items @OneToMany(mappedBy, cascade = ALL, orphanRemoval = true), List<OrderItem> @MappedCollection(idColumn, keyColumn), List<OrderItem>
Identity of a child row @ManyToOne back-reference to its parent none — a JDBC OrderItem has no idea which Order owns it
Optimistic locking @Version Long version @Version Long version (same annotation, same package: org.springframework.data.annotation)

Both sides run against the same H2 database through the same StatementLoggingDataSource — see 03 — the SQL log for why that matters for the numbers in the article.

A casing trap that cost a debugging session

Spring Data JDBC treats an explicit @Table("some_name") or @Column("some_name") value as a literal, quoted SQL identifier — it is rendered exactly as given, in double quotes. Properties with no explicit annotation go through the H2 dialect's default identifier processing instead, which upper-cases them. H2 itself upper-cases any unquoted identifier in DDL. The result: an unquoted create table jdbc_customer(...) in schema.sql produces a table H2 privately calls JDBC_CUSTOMER, but @Table("jdbc_customer") generates INSERT INTO "jdbc_customer" (...) — quoted, lower-case, and therefore a different identifier as far as H2 is concerned. The fix used throughout this module is to give every explicit @Table/@Column/@MappedCollection value in upper case, matching H2's own folding, so the literal and the folded name agree. See jdbc/Order.java for the annotations and schema.sql for the DDL. This is exactly the kind of thing that never shows up in a tutorial that only ever runs one save and eyeballs the console.

Continue to 02 — the JPA side.