Files
hibernate-demo/README.md
T

20 KiB
Executable File
Raw Blame History

hibernate-demo

Companion repository for a batch of eighteen ankurm.com chapters on Hibernate 7: the persistence-context APIs (get() vs getReference(), merge() vs refresh(), batch inserts), mapping styles (annotations vs. XML, JPA persistence annotations, natural ids, immutable entities), a set of deeper operational topics (stored procedures, in-memory test databases, mocking JNDI datasources, proxies and lazy initialization, association mappings, date/time mapping, and named queries), and the query layer (HQL, the Criteria API with a real generated metamodel, and bootstrapping EntityManagerFactory outside Spring entirely). Every claim in those chapters that comes from this repo traces to a named JUnit test here and a captured transcript in docs/output/ — nothing is asserted that wasn't actually run.

Versions

Component Version Notes
Spring Boot 4.1.1 parent POM
Hibernate ORM 7.4.5.Final what Boot 4.1.1 manages — see docs/00-versions.md for why this is not the same patch Boot 4.1.0 resolves
Spring Framework 7.0.9 pulled in transitively by Boot 4.1.1
Jakarta Persistence 3.2.0 jakarta.persistence-api
Java 25 (Temurin 25.0.4.1 LTS)
H2 2.4.240 in-memory, DB_CLOSE_DELAY=-1
HSQLDB 2.7.3 in-memory
Apache Derby 10.16.1.1 in-memory; needs hibernate-community-dialects (see below)
simple-jndi 0.25.0 com.github.h-thurow:simple-jndi, test scope — not the abandoned simple-jndi:simple-jndi groupId
JUnit Jupiter 6.0.3 via spring-boot-starter-test

This repo was bumped this session from Boot 4.1.0 / Hibernate 7.4.1.Final to Boot 4.1.1 / Hibernate 7.4.5.Final. If you see either of those older numbers anywhere outside docs/00-versions.md's own explanation of the bump, that's stale and should be corrected — the live pom.xml and every chapter added or touched this session already reflect 4.1.1 / 7.4.5.Final.

See docs/00-versions.md for how these were verified (against maven-metadata.xml, not Maven Central's search index) and the trap worth knowing about if you bump the Spring Boot version again: two different Boot 4.1.x patch releases can resolve two different Hibernate patch releases from the same "4.1" line.

Quickstart

git clone https://ankurm.com/git.app/asmhatre/hibernate-demo.git
cd hibernate-demo
./mvnw test

Requires JDK 25 and a network connection the first time (Maven needs to fetch plugins online before -o offline mode works for subsequent runs). mvn test runs the full suite — 141 tests, 0 failures as of the last commit — and takes a couple of minutes, since it boots Hibernate against H2, HSQLDB and Derby repeatedly, spins up a real embedded Tomcat for the OSIV chapter, and runs a 5,500-iteration timing comparison for the named-query chapter.

Two configuration decisions in this repo are easy to trip over if you're extending it rather than just reading it:

  • hibernate.cache.use_second_level_cache: false is pinned explicitly in application.yml, on purpose. hibernate-jcache is on this project's classpath (chapter 06's natural-id L2 measurements need it), and Hibernate 7.4.5 turns the second-level cache on by itself, for every context in the repo, the moment it finds a RegionFactory via the service loader — with nothing configured anywhere. Before this was pinned, that silently broke an unrelated test (see chapter 09 for the verbatim failure and the JCacheOnClasspathAutoEnablesL2Test that pins it down). If you add a JCache/Ehcache/Infinispan dependency to a Hibernate project for one narrow purpose, assume it just turned L2 caching on globally unless you pin this back down yourself.
  • hibernate-community-dialects is a required runtime dependency, not an optional add-on, because org.hibernate.dialect.DerbyDialect no longer exists in hibernate-core as of Hibernate 6.2+. Derby's dialect moved to a different package, org.hibernate.community.dialect.DerbyDialect, in this separate artifact — see chapter 09 for both failure messages this produces if the dependency or the dialect setting is missing.

Three databases, and why

H2, HSQLDB and Derby are all exercised side by side (chapter 09 in particular boots all three against the identical entity mapping), specifically to find where their dialect emulation actually diverges rather than repeating folklore about it — the reserved-word survey, the CHAR-padding behavior, and the Derby dialect relocation above are all things that only showed up by running all three, not by reading about one of them. Testcontainers is discussed but not runnable in the sandbox this repo was built in — there is no Docker binary and no /var/run/docker.sock available, so nothing Testcontainers-based was attempted or claimed to have been run. The in-memory engines cover fast, unit-level ORM testing; a real production database via Testcontainers is a separate, slower integration stage this repo doesn't attempt to substitute for.

Chapter index

Chapter Covers
00 — Versions Verified version pins, and the Spring Boot patch that silently changes which Hibernate patch you get
01 — get() vs getReference() The 4-calls-4-outcomes table, the same-session matrix, proxy identity vs equals()
02 — merge() vs refresh() Which method fails loudly vs silently, exactly when the optimistic-lock check fires, and what a cascaded LAZY collection does under merge()
03 — Hibernate 7 batch inserts IDENTITY vs SEQUENCE, the allocationSize and batch_size sweeps, Session vs StatelessSession
04 — Annotations vs. XML mappings hbm.xml still works (WARN, not broken), orm.xml can define a whole entity, XML always wins on conflict, and the third XML dialect (mapping.xml) neither older article mentions
05 — JPA persistence annotations @Temporal's formal deprecation, the ORDINAL enum reorder failure reproduced, @JdbcTypeCode(JSON)'s hidden Jackson dependency, the surrogate-id HashSet trap, mixed access-type gotchas, and what's actually new in Jakarta Persistence 3.2
06 — Natural IDs bySimpleNaturalId, L1 vs L2 cache behavior measured with real query counts, the "L2 populates on INSERT, not first lookup" surprise, enforced natural-id immutability, and composite natural ids
07 — Immutable entities @Immutable's actual mechanism (dirty-check exclusion, not a write guard), what it does and doesn't block, and a measured flush-cost comparison against setReadOnly()
08 — Stored procedures IN/OUT/INOUT against a real HSQLDB procedure, a genuine HSQLDB-driver result-set incompatibility, the parameter name-vs-position trap, and why procedure calls don't auto-flush
09 — Testing with in-memory databases Derby's relocated dialect, the hibernate-jcache classpath-pollution trap, the real (and not-Derby-exclusive) cross-database divergences, and schema/test isolation strategies
10 — Mocking JNDI datasources Why SimpleNamingContextBuilder is gone, getting simple-jndi 0.25.0 actually working, the failure modes verbatim, and whether mock JNDI is still the right answer in 2026
11 — Proxies and lazy initialization What a HibernateProxy actually is, both LazyInitializationException message templates, fetchgraph vs loadgraph demonstrated, and what OSIV actually masks
12 — Association mappings N+1 counted exactly, MultipleBagFetchException and its fixes, the cartesian-product trap, the @OneToOne lazy trap, and cascade/orphanRemoval's real (and not-so-real) failure modes
13 — Date and time mapping Every basic temporal type round-tripped, the six TimeZoneStorageType constants measured under a JVM zone change, nanosecond rounding vs. truncation across databases, and hibernate.jdbc.time_zone's raw-storage effect
14 — Named queries Startup validation's two different failure modes, Hibernate's extra @NamedQuery attributes, orm.xml named queries, and a measured (negative) answer to "are named queries faster?"
15 — HQL queries The column-name-vs-field-name pitfall, JOIN vs JOIN FETCH, aggregates/pagination, bulk UPDATE/DELETE bypassing the persistence context, and what each flush mode actually suppresses (including why GenerationType.IDENTITY defeats an INSERT-based flush-mode test)
16 — Criteria API The real generated static metamodel (Employee_/Department_) vs. string paths, root.join() vs root.fetch(), aggregation and subqueries, CriteriaUpdate/CriteriaDelete, and when Criteria earns its ceremony over HQL
17 — Bootstrapping EntityManager XML vs. Jakarta Persistence 3.2's PersistenceConfiguration, proof that a reused persistence-unit name does not trigger an XML lookup, the corrected jakarta.persistence.PersistenceException type, and a measured factory-vs-EntityManager creation cost

Every surprising claim from chapters 01–03, mapped to a test

Each row is one ./mvnw -Dtest=ClassName test away from reproducing itself.

Test class Backs post Proves
GetVsGetReferenceTest 4859 (get vs getReference) The 4-calls-4-outcomes table, the same-session matrix, proxy identity vs equals()
MergeRefreshTest 4860 (merge vs refresh) merge() returns the pre-existing managed instance; merge() initializes a cascaded LAZY collection; refresh() silently discards an unflushed edit
OptimisticLockTest 4860 (merge vs refresh) Exactly when OptimisticLockException surfaces relative to merge()/commit
IdentityBatchTest 4861 (batch inserts) GenerationType.IDENTITY disables batching entirely
SequenceBatchTest 4861 (batch inserts) GenerationType.SEQUENCE allows real batching
AllocationSizeSweepTest 4861 (batch inserts) allocationSize sweep (1, 10, 25, 50) at fixed batch_size=25
BatchSizeSweepTest 4861 (batch inserts) batch_size sweep (1, 10, 25, 50) at fixed allocationSize=50

For chapters 04–17, the test classes are linked directly from each chapter rather than duplicated here — every source file, test class, and captured output file a chapter discusses is a relative link in that chapter.

./mvnw -Dtest=GetVsGetReferenceTest test
./mvnw -Dtest=MergeRefreshTest,OptimisticLockTest test
./mvnw -Dtest=IdentityBatchTest,SequenceBatchTest test
./mvnw -Dtest=AllocationSizeSweepTest test
./mvnw -Dtest=BatchSizeSweepTest test
./mvnw -Dtest=HqlQueryTest test
./mvnw -Dtest=CriteriaQueryTest test
./mvnw -Dtest=EntityManagerBootstrapTest test
./mvnw test   # the whole suite, 169 tests as of the last commit — see "Known issues" below

Known issues

OrmXmlNamedQueryTest (chapter 14) currently fails, and it predates chapters 15–17. Verified by stashing every change made for chapters 15–17 and re-running the suite against the unmodified tree: the same two errors reproduce identically (No query named 'XmlQueryEmployee.findBySalaryAboveXml' and No parameter named ':min' in query with named parameters []), so this is not a regression from the HQL/Criteria/bootstrap work. It contradicts chapter 14's own claim that orm.xml named queries are "auto-discovered with no persistence.xml" under Spring Boot's default JPA autoconfiguration — that claim needs re-verification against the current Hibernate/Spring Boot versions (this repo was bumped mid-batch from Boot 4.1.0/Hibernate 7.4.1.Final to Boot 4.1.1/Hibernate 7.4.5.Final, which is one plausible place the behavior changed) before it's trusted again. Flagged here rather than fixed silently, since chapter 14 and its post (4877) are outside this batch's scope.

CommandLineRunner scenarios

The original narrative scenarios (chapters 01–03 only) are still here, unchanged, for anyone who wants to read a straight-line script instead of a test class:

Profile Runs Chapter
getvsload session.get() vs session.getReference(), proxies, LazyInitializationException docs/01-get-vs-load.md
mergerefresh merge() vs refresh() against a @Version-ed entity docs/02-merge-vs-refresh.md
insert-identity Batch insert attempt with GenerationType.IDENTITY docs/03-inserting-objects.md
insert-sequence The same insert, with GenerationType.SEQUENCE docs/03-inserting-objects.md
./scripts/run.sh getvsload
./scripts/run.sh mergerefresh
./scripts/run.sh insert-identity
./scripts/run.sh insert-sequence

Chapters 04–17 don't have CommandLineRunner profiles — they're covered entirely by JUnit tests and javap/unzip probes, captured the same way (see below).

Regenerating captured output

./scripts/run-all.sh

Regenerates the CommandLineRunner-scenario files in docs/output/ (chapters 01–03 only). scripts/clean_output.py strips JVM noise and a harmless duplicate SQL echo line so the committed transcripts stay readable — nothing else is edited by hand. Every other transcript in docs/output/ (chapters 04–17, and the test-suite transcripts for 01–03) was captured directly from ./mvnw -Dtest=... test, javap, or unzip -l piped through the same cleanup approach, not regenerated by a single script — see each chapter for the exact command that produced its output.

Output index

docs/output/ holds 63 raw, unedited transcripts, grouped here by topic prefix.

Prefix Files What it captures
get-vs-* 2 Chapter 01: the CommandLineRunner scenario and the GetVsGetReferenceTest run
merge-vs-refresh* 2 Chapter 02: the CommandLineRunner scenario and the MergeRefreshTest/OptimisticLockTest run
insert-* 2 Chapter 03: the identity vs. sequence CommandLineRunner scenarios
allocation-and-batch-size-sweeps.txt 1 Chapter 03: the allocationSize and batch_size sweep test run
mappingstyle-* 1 Chapter 04: the full hbm.xml/orm.xml/mapping.xml test run
persistenceannotations-* 1 Chapter 05: the full persistence-annotations test run
naturalid-* 1 Chapter 06: the full natural-id L1/L2/mutability/composite test run
immutable-* 2 Chapter 07: the @Immutable behavior run, and the javap capture of the annotation itself
procedure-* 4 Chapter 08: happy-path and failure-mode runs, the HSQLDB driver quirk reproduction, and the javap API-surface capture
testdb-* 9 Chapter 09: dialect resolution, DDL generation, the reserved-word survey, cross-database behavior, schema isolation, startup timing, the Derby dialect failure, and the jcache classpath-pollution failure
jndi-* 6 Chapter 10: the SimpleNamingContextBuilder removal proof, the simple-jndi jar listing, Boot's JNDI autoconfigure javap capture, Hibernate's own JNDI datasource javap capture, and both the filtered and unfiltered full test runs
proxy-* 7 Chapter 11: proxy identity and LazyInitializationException (filtered and raw), entity-graph fetch behavior (filtered and raw), OSIV masking (filtered and raw), and the javap capture of proxy-related settings
association-* 4 Chapter 12: N+1 counting, the multiple-bag-fetch/cartesian-product runs, the @OneToOne lazy trap, and cascade/orphanRemoval
datetime-* 7 Chapter 13: basic temporal round trips, the @Temporal deprecation warning, both @TimeZoneStorage JVM-zone runs, nanosecond rounding on both H2 and HSQLDB, and hibernate.jdbc.time_zone
namedquery-* 4 Chapter 14: startup validation, execution/projections, orm.xml named queries, and the pre-parsing performance comparison
hql-* 3 Chapter 15: select/join pitfalls, aggregation/paging/bulk operations, and flush-mode behavior
criteria-* 3 Chapter 16: predicates and the static metamodel, aggregation/subqueries, and bulk update/delete
bootstrap-persistenceconfiguration.txt 1 Chapter 17: XML and programmatic bootstrap, the persistence-unit name-collision proof, the unconfigured-name exception, and the factory-creation timing

Layout

hibernate-demo/
├── pom.xml
├── LICENSE
├── scripts/
│   ├── run.sh              start one CommandLineRunner profile, run it, exit
│   ├── run-all.sh          regenerate the CommandLineRunner docs/output/ files
│   ├── clean_output.py     strip JVM noise + a duplicate SQL echo line from a raw capture
│   └── check_links.py      walk docs/*.md and verify every relative link resolves on disk
├── src/main/java/com/ankurm/hibernatedemo/
│   ├── HibernateDemoApplication.java
│   ├── model/               Book, Note, Widget* entities (chapters 01–03)
│   ├── scenario/            one CommandLineRunner per profile (chapters 01–03)
│   ├── mappingstyle/        chapter 04 entities
│   ├── persistenceannotations/  chapter 05 entities
│   ├── naturalid/           chapter 06 entities
│   ├── immutable/           chapter 07 entities
│   ├── procedure/           chapter 08 entities
│   ├── testdb/              chapter 09 entity
│   ├── proxy/               chapter 11 entities
│   ├── association/         chapter 12 entities
│   ├── datetime/            chapter 13 entities
│   ├── namedquery/          chapter 14 entities
│   ├── query/               chapters 15–16 entities (Employee, Department — @Entity(name="QueryDept"))
│   └── bootstrap/           chapter 17 entity (BootstrapUser)
├── src/test/java/com/ankurm/
│   ├── brokenprobe/         a deliberately broken entity kept outside the scanned package (chapter 14)
│   └── hibernatedemo/
│       ├── GetVsGetReferenceTest.java, MergeRefreshTest.java, OptimisticLockTest.java,
│       │   IdentityBatchTest.java, SequenceBatchTest.java, AllocationSizeSweepTest.java,
│       │   BatchSizeSweepTest.java, ImmutableEntityTest.java, ImmutableBulkUpdateAllowedTest.java,
│       │   ImmutableFlushCostTest.java   (chapters 01–03, 07)
│       ├── mappingstyle/, persistenceannotations/, naturalid/, procedure/, testdb/,
│       │   proxy/, association/, datetime/, namedquery/, jndi/   (chapters 04–06, 08–14)
│       ├── query/           HqlQueryTest.java, CriteriaQueryTest.java   (chapters 15–16)
│       └── bootstrap/       EntityManagerBootstrapTest.java, plain JUnit, no Spring   (chapter 17)
└── docs/
    ├── 00-versions.md .. 17-entitymanager-bootstrap.md
    └── output/*.txt         captured, unedited console transcripts (63 files)

License

MIT — see LICENSE.