From 5533c6c8fe63ad61a908c63ae205e001a306c671 Mon Sep 17 00:00:00 2001 From: Ankur Mhatre Date: Wed, 26 Aug 2026 17:26:04 +0000 Subject: [PATCH] Add hibernate-demo: get() vs load(), merge() vs refresh(), inserting objects (Hibernate 7.4.1.Final + Spring Boot 4.1.0) --- .gitignore | 6 + LICENSE | 21 ++ README.md | 99 +++++++ docs/00-versions.md | 45 +++ docs/01-get-vs-load.md | 98 +++++++ docs/02-merge-vs-refresh.md | 108 +++++++ docs/03-inserting-objects.md | 263 ++++++++++++++++++ docs/output/get-vs-load.txt | 46 +++ docs/output/insert-identity.txt | 71 +++++ docs/output/insert-sequence.txt | 104 +++++++ docs/output/merge-vs-refresh.txt | 41 +++ pom.xml | 66 +++++ scripts/clean_output.py | 47 ++++ scripts/run-all.sh | 27 ++ scripts/run.sh | 18 ++ .../HibernateDemoApplication.java | 27 ++ .../com/ankurm/hibernatedemo/model/Book.java | 69 +++++ .../hibernatedemo/model/WidgetIdentity.java | 41 +++ .../hibernatedemo/model/WidgetSequence.java | 42 +++ .../scenario/GetVsLoadRunner.java | 154 ++++++++++ .../scenario/InsertIdentityRunner.java | 58 ++++ .../scenario/InsertSequenceRunner.java | 55 ++++ .../scenario/MergeVsRefreshRunner.java | 113 ++++++++ src/main/resources/application.yml | 34 +++ 24 files changed, 1653 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/00-versions.md create mode 100644 docs/01-get-vs-load.md create mode 100644 docs/02-merge-vs-refresh.md create mode 100644 docs/03-inserting-objects.md create mode 100644 docs/output/get-vs-load.txt create mode 100644 docs/output/insert-identity.txt create mode 100644 docs/output/insert-sequence.txt create mode 100644 docs/output/merge-vs-refresh.txt create mode 100644 pom.xml create mode 100755 scripts/clean_output.py create mode 100755 scripts/run-all.sh create mode 100755 scripts/run.sh create mode 100644 src/main/java/com/ankurm/hibernatedemo/HibernateDemoApplication.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/model/Book.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java create mode 100644 src/main/java/com/ankurm/hibernatedemo/scenario/MergeVsRefreshRunner.java create mode 100644 src/main/resources/application.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b13f319 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +target/ +*.class +.idea/ +*.iml +.vscode/ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa5473f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Ankur Mhatre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..4c7d6c4 --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# hibernate-demo + +Companion repository for three ankurm.com posts on Hibernate 7's persistence-context APIs: +`get()` vs `load()`/`getReference()`, `merge()` vs `refresh()`, and inserting objects +efficiently. Every claim in those posts that comes from this repo traces to a script here and a +captured transcript in `docs/output/` — nothing is asserted that wasn't actually run. + +## Versions + +| Component | Version | +|---|---| +| Hibernate ORM | `7.4.1.Final` (GA 2026-06-09) | +| Spring Boot | `4.1.0` (GA 2026-06-10) | +| Java | `25` (LTS) | +| H2 | in-memory, managed by Spring Boot | + +See [`docs/00-versions.md`](docs/00-versions.md) for how these were verified and a trap worth +knowing about if you bump the Spring Boot version. + +## Quickstart + +```bash +git clone https://ankurm.com/git.app/asmhatre/hibernate-demo.git +cd hibernate-demo +./scripts/run.sh getvsload +``` + +Requires JDK 25 and a network connection the first time (Maven needs to fetch plugins online +before `-o` offline mode works for subsequent runs). + +## Profiles + +Each profile is a `CommandLineRunner` against an in-memory H2 database. There is no web server — +every run starts, executes its scenario, prints the result, and exits. + +| Profile | Runs | Chapter | +|---|---|---| +| `getvsload` | `session.get()` vs `session.getReference()`, proxies, `LazyInitializationException` | [docs/01-get-vs-load.md](docs/01-get-vs-load.md) | +| `mergerefresh` | `merge()` vs `refresh()` against a `@Version`-ed entity | [docs/02-merge-vs-refresh.md](docs/02-merge-vs-refresh.md) | +| `insert-identity` | Batch insert attempt with `GenerationType.IDENTITY` | [docs/03-inserting-objects.md](docs/03-inserting-objects.md) | +| `insert-sequence` | The same insert, with `GenerationType.SEQUENCE` | [docs/03-inserting-objects.md](docs/03-inserting-objects.md) | + +```bash +./scripts/run.sh getvsload +./scripts/run.sh mergerefresh +./scripts/run.sh insert-identity +./scripts/run.sh insert-sequence +``` + +## Regenerating captured output + +```bash +./scripts/run-all.sh +``` + +Regenerates every file in `docs/output/` from a real run. `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. + +## Documentation index + +| Chapter | Covers | +|---|---| +| [00 — Versions](docs/00-versions.md) | Verified version pins, and the Spring Boot patch that silently changes which Hibernate patch you get | +| [01 — get() vs load()](docs/01-get-vs-load.md) | Proxy deferral, `LazyInitializationException`, proxy identity vs `equals()` | +| [02 — merge() vs refresh()](docs/02-merge-vs-refresh.md) | Optimistic locking, which method fails loudly vs silently | +| [03 — Inserting objects](docs/03-inserting-objects.md) | `IDENTITY` vs `SEQUENCE` and JDBC batching, with `hibernate.generate_statistics` as evidence | + +## Captured output index + +| File | Scenario | +|---|---| +| [docs/output/get-vs-load.txt](docs/output/get-vs-load.txt) | `getvsload` | +| [docs/output/merge-vs-refresh.txt](docs/output/merge-vs-refresh.txt) | `mergerefresh` | +| [docs/output/insert-identity.txt](docs/output/insert-identity.txt) | `insert-identity` | +| [docs/output/insert-sequence.txt](docs/output/insert-sequence.txt) | `insert-sequence` | + +## Layout + +``` +hibernate-demo/ +├── pom.xml +├── LICENSE +├── scripts/ +│ ├── run.sh start one profile, run it, exit +│ ├── run-all.sh regenerate every docs/output/ file +│ └── clean_output.py strip JVM noise + a duplicate SQL echo line from a raw capture +├── src/main/java/com/ankurm/hibernatedemo/ +│ ├── HibernateDemoApplication.java +│ ├── model/ Book, WidgetIdentity, WidgetSequence +│ └── scenario/ one CommandLineRunner per profile +└── docs/ + ├── 00-versions.md .. 03-inserting-objects.md + └── output/*.txt captured, unedited console transcripts +``` + +## License + +MIT — see [LICENSE](LICENSE). diff --git a/docs/00-versions.md b/docs/00-versions.md new file mode 100644 index 0000000..3dc563c --- /dev/null +++ b/docs/00-versions.md @@ -0,0 +1,45 @@ +# 00 — Versions + +[Next: 01 — get() vs load() →](01-get-vs-load.md) + +This repository is pinned to: + +| Component | Version | GA date | Source | +|---|---|---|---| +| Hibernate ORM | `7.4.1.Final` | 2026-06-09 | [hibernate.org/orm/releases/7.4](https://hibernate.org/orm/releases/7.4/) | +| Spring Boot | `4.1.0` | 2026-06-10 | [spring.io/blog/2026/06/10/spring-boot-4](https://spring.io/blog/2026/06/10/spring-boot-4/) | +| Java | `25` (LTS) | 2025-09 | latest LTS at the time this repo was built | +| H2 | managed by Spring Boot 4.1.0 | — | in-memory, `DB_CLOSE_DELAY=-1` | + +## The pin lines up, but check before you assume it always will + +`hibernate-core`'s own `maven-metadata.xml` on Maven Central lists `7.4.6.Final` as the newest +GA release at the time this was written — several patches ahead of `7.4.1.Final`. This repo +pins to `7.4.1.Final` deliberately, because that is the version this batch of posts was written +and run against, and because it is *exactly* the version Spring Boot 4.1.0 resolves on its own. + +That last part is not a coincidence to take for granted, though. Checking +`spring-boot-dependencies-4.1.0.pom` directly shows `7.4.1.Final` — +so on Boot 4.1.0, `pom.xml` in this repo does not need to override anything to get 7.4.1.Final; +the `` property declared here is redundant with what Boot already resolves, +kept only so the pin is visible without cracking open Boot's own POM. + +That stops being true one patch release later. `spring-boot-dependencies-4.1.1.pom` resolves +`hibernate.version` to `7.4.5.Final` — a different Hibernate patch from the same Spring Boot +minor version, four Hibernate patch releases apart. If you bump this repo's parent to `4.1.1` +without touching the `` property, you get `7.4.1.Final` back (the explicit +property now *does* override Boot's own management) rather than the `7.4.5.Final` Boot intended +you to get — which is a more useful trap to know about than to fall into. + +| Spring Boot version | Hibernate version Boot resolves | +|---|---| +| `4.0.8` | `7.2.24.Final` | +| `4.1.0` | `7.4.1.Final` | +| `4.1.1` | `7.4.5.Final` | + +Verified against `maven-metadata.xml` on `repo1.maven.org`, not against Maven Central's +`solrsearch` API — that index has been observed stale from this kind of sandboxed build +environment (it reported an old Spring Boot release as newest well after a later one had +shipped), so it should not be trusted for currency checks. + +[Next: 01 — get() vs load() →](01-get-vs-load.md) diff --git a/docs/01-get-vs-load.md b/docs/01-get-vs-load.md new file mode 100644 index 0000000..c9a8255 --- /dev/null +++ b/docs/01-get-vs-load.md @@ -0,0 +1,98 @@ +# 01 — get() vs load() / getReference() + +[← Previous: 00 — Versions](00-versions.md) | [Next: 02 — merge() vs refresh() →](02-merge-vs-refresh.md) + +Backs [ankurm.com: Hibernate 7 — get() vs load()](https://ankurm.com/hibernate-7-get-vs-load-which-one-should-you-actually-use/). + +Source: [`GetVsLoadRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java). +Run it yourself with `./scripts/run.sh getvsload`; the full transcript below is committed at +[`docs/output/get-vs-load.txt`](output/get-vs-load.txt). + +## What the six steps show + +1. **`session.get()` on an existing id** — one `SELECT` immediately, returns the real entity. +2. **`session.get()` on a missing id** — one `SELECT`, returns `null`. No exception, ever. +3. **`session.getReference()` on an existing id** — no `SELECT` at the call site. The `SELECT` + only happens the moment a non-identifier accessor (`getTitle()`) is called on the returned + proxy. The log ordering in the transcript is the actual evidence for this, not a claim about + it — the `about to call` / `now calling` markers in the code bracket exactly where each + `SELECT` does or doesn't appear. +4. **`session.getReference()` on a missing id** — same deferral: the proxy is handed back with + no exception, and only accessing it throws + `jakarta.persistence.EntityNotFoundException: No row with the given identifier exists`. +5. **A proxy outlives its session** — `getReference()`, then commit and close, then access: + `org.hibernate.LazyInitializationException: Could not initialize proxy [...] - no session`. +6. **Proxy identity** — `Book.class.isInstance(proxy)` is `true`, but `real.getClass() == + proxy.getClass()` is `false` (the proxy's runtime class is `Book$HibernateProxy`), and + `real.equals(proxy)` is `false` even though both represent the same row. + +## What surprised me building this + +Step 6 is the one worth sitting with. `Book` here does not override `equals()`, so +`real.equals(proxy)` falling through to reference equality is not a Hibernate quirk — it's plain +Java doing exactly what an un-overridden `equals()` always does. But it means the moment `get()` +and `getReference()` are mixed for the *same row* in code that ever compares entities by +`equals()` (a `Set`, a `List.contains()`, a manual reconciliation), the proxy boundary silently +breaks that comparison. `instanceof` survives it. `equals()`, `hashCode()`-based collections, and +naive `==` do not. The fix is the one every Effective-Java-style guide already recommends — +implement `equals()`/`hashCode()` off the identifier, never off the object's identity — but this +is what makes skipping that advice actually bite: not a compile error, a `false` you don't +notice until two supposedly-identical entities land in the same `HashSet` and both show up. + +Also unexpected going in: `getReference()` on a *missing* row throws `EntityNotFoundException` +on first non-id access, not `ObjectNotFoundException`. The two names get used interchangeably in +older Hibernate discussion; running it pins down which one this exact version actually throws +(see the transcript for the full class name and message). + +## Full captured transcript + +```console +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +Hibernate: select next value for book_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,title,version,id) values (?,?,?,?) +binding parameter (1:VARCHAR) <- [Joshua Bloch] +binding parameter (2:VARCHAR) <- [Effective Java] +binding parameter (3:BIGINT) <- [0] +binding parameter (4:BIGINT) <- [1] +SEED: inserted Book id=1 +--- Step 1: session.get() on an existing id --- +about to call session.get(Book.class, 1) +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +get() returned: Book{id=1, title=Effective Java, author=Joshua Bloch, version=0} +--- Step 2: session.get() on a missing id --- +about to call session.get(Book.class, 999001) +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [999001] +get() returned: null (no exception thrown) +--- Step 3: session.getReference() on an existing id --- +getReference() returned proxy of class com.ankurm.hibernatedemo.model.Book$HibernateProxy -- no SELECT above this line +now calling proxy.getTitle() ... +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +getTitle() returned 'Effective Java' -- the SELECT for this ran just above this line +--- Step 4: session.getReference() on a missing id --- +getReference() returned a proxy for a row that does not exist -- no exception yet: com.ankurm.hibernatedemo.model.Book$HibernateProxy +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [999001] +accessing the proxy threw jakarta.persistence.EntityNotFoundException: No row with the given identifier exists for entity [com.ankurm.hibernatedemo.model.Book with id '999001'] +--- Step 5: proxy accessed after its session is closed --- +session closed. proxy in hand: com.ankurm.hibernatedemo.model.Book$HibernateProxy +accessing the proxy after close threw org.hibernate.LazyInitializationException: Could not initialize proxy [com.ankurm.hibernatedemo.model.Book#1] - no session +--- Step 6: proxy identity vs a real loaded instance --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +real.getClass() = com.ankurm.hibernatedemo.model.Book +proxy.getClass() = com.ankurm.hibernatedemo.model.Book$HibernateProxy +proxy instanceof Book.class: true +real.getClass() == proxy.getClass(): false +real.equals(proxy) before proxy access: false +``` + +[← Previous: 00 — Versions](00-versions.md) | [Next: 02 — merge() vs refresh() →](02-merge-vs-refresh.md) diff --git a/docs/02-merge-vs-refresh.md b/docs/02-merge-vs-refresh.md new file mode 100644 index 0000000..996380a --- /dev/null +++ b/docs/02-merge-vs-refresh.md @@ -0,0 +1,108 @@ +# 02 — merge() vs refresh() + +[← Previous: 01 — get() vs load()](01-get-vs-load.md) | [Next: 03 — Inserting objects →](03-inserting-objects.md) + +Backs [ankurm.com: merge() vs refresh()](https://ankurm.com/mastering-hibernate-7-merging-vs-refreshing-entities-for-robust-data-consistency/). + +Source: [`MergeVsRefreshRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/MergeVsRefreshRunner.java), +entity: [`Book`](../src/main/java/com/ankurm/hibernatedemo/model/Book.java) (note the `@Version` +column — this scenario specifically needs a real optimistic-lock field, not just a plain one). +Run it yourself with `./scripts/run.sh mergerefresh`; the full transcript is committed at +[`docs/output/merge-vs-refresh.txt`](output/merge-vs-refresh.txt). + +## The setup + +1. Seed one `Book` row, `version=0`. +2. Load it, then close the session — it is now **detached**, still holding `version=0`. +3. A second, independent session loads the same row, edits the title, and commits. The row's + `version` in the database is now `1`. +4. Back on the original detached instance (still at `version=0`), edit a *different* field + (`author`) and call `session.merge(detached)`. +5. Separately: load the row fresh into a **managed** session, make an in-memory edit, don't + flush it, then call `session.refresh(...)`. + +## What actually happened + +Step 4 does not silently overwrite the title from step 3. It throws: + +``` +jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another +transaction for entity [com.ankurm.hibernatedemo.model.Book with id '1'] +``` + +`merge()` re-fetches the current row, sees the database is at `version=1` while the detached +instance is still carrying `version=0`, and refuses the write. The title edited by the "other +process" in step 3 survives untouched. + +Step 5 is the opposite outcome for a structurally similar situation. The managed entity has an +unflushed local edit (`author` set to a new value that was never sent to the database). +`refresh()` re-runs the `SELECT` and overwrites every field with what's in the database right +now — including the field with the pending edit. No exception. No warning. The edit is just +gone: + +``` +before refresh(): Book{id=1, title=Clean Code (2nd Edition), author=SOMEONE ELSE ENTIRELY (never flushed), version=1} +after refresh(): Book{id=1, title=Clean Code (2nd Edition), author=Robert C. Martin, version=1} -- the local edit is gone, no exception was thrown +``` + +## What surprised me building this + +Going in, the plan was to show "merge() can silently overwrite concurrent changes" as the +headline risk — that's the framing most write-ups of `merge()` use. Running it against a +`@Version`-ed entity showed the opposite: with optimistic locking in place, `merge()` is the +*safe* one here — it throws rather than clobbering someone else's committed write. `refresh()` +is the one that destroys data silently, and it does it to an edit that was never even sent to +the database yet. The risk isn't "which method can overwrite the database" — both can, that's +their job. It's "which method fails loudly when the state it's holding is stale," and on a +versioned entity that is `refresh()`, not `merge()` — precisely backwards from how the pairing +usually gets described. Strip the `@Version` column out and this flips: an unversioned `merge()` +would apply the stale write in step 4 without a peep. The column is not incidental to the +result; it's the whole reason the result is what it is. + +## Full captured transcript + +```console +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +Hibernate: select next value for book_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,title,version,id) values (?,?,?,?) +binding parameter (1:VARCHAR) <- [Robert C. Martin] +binding parameter (2:VARCHAR) <- [Clean Code] +binding parameter (3:BIGINT) <- [0] +binding parameter (4:BIGINT) <- [1] +SEED: inserted Book{id=1, title=Clean Code, author=Robert C. Martin, version=0} +--- Step 1: load the row, then close the session (entity is now detached) --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +detached instance in hand: Book{id=1, title=Clean Code, author=Robert C. Martin, version=0} +--- Step 2: a second, independent session edits the same row and commits --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +Hibernate: /* update for com.ankurm.hibernatedemo.model.Book */update book set author=?,title=?,version=? where id=? and version=? +binding parameter (1:VARCHAR) <- [Robert C. Martin] +binding parameter (2:VARCHAR) <- [Clean Code (2nd Edition)] +binding parameter (3:BIGINT) <- [1] +binding parameter (4:BIGINT) <- [1] +binding parameter (5:BIGINT) <- [0] +second session committed: Book{id=1, title=Clean Code (2nd Edition), author=Robert C. Martin, version=1} -- version column has now advanced in the database +--- Step 3: mutate the ORIGINAL detached instance (still holding the OLD version) and merge() it --- +detached instance before merge (note the version and title are both stale): Book{id=1, title=Clean Code, author=Robert C. Martin (Uncle Bob), version=0} +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +merge() threw jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.ankurm.hibernatedemo.model.Book with id '1'] +the title change from Step 2 survives untouched -- merge() refused to apply a write built on a stale version +--- Step 4: refresh() on a MANAGED entity with an unflushed local edit --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +before refresh(): Book{id=1, title=Clean Code (2nd Edition), author=SOMEONE ELSE ENTIRELY (never flushed), version=1} +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +after refresh(): Book{id=1, title=Clean Code (2nd Edition), author=Robert C. Martin, version=1} -- the local edit is gone, no exception was thrown +``` + +[← Previous: 01 — get() vs load()](01-get-vs-load.md) | [Next: 03 — Inserting objects →](03-inserting-objects.md) diff --git a/docs/03-inserting-objects.md b/docs/03-inserting-objects.md new file mode 100644 index 0000000..25053d0 --- /dev/null +++ b/docs/03-inserting-objects.md @@ -0,0 +1,263 @@ +# 03 — Inserting objects efficiently + +[← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md) + +Backs [ankurm.com: Inserting objects efficiently](https://ankurm.com/mastering-hibernate-7-the-ultimate-guide-to-inserting-objects-efficiently/). + +Sources: [`InsertIdentityRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java) / +[`InsertSequenceRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java), +entities [`WidgetIdentity`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java) / +[`WidgetSequence`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java). +Run both with `./scripts/run.sh insert-identity` and `./scripts/run.sh insert-sequence`. +Transcripts: [`docs/output/insert-identity.txt`](output/insert-identity.txt), +[`docs/output/insert-sequence.txt`](output/insert-sequence.txt). + +## The one-line difference that matters + +Both entities persist 30 rows in a single transaction with **identical** settings: + +```yaml +hibernate.jdbc.batch_size: 25 +hibernate.order_inserts: true +``` + +`WidgetIdentity` uses `@GeneratedValue(strategy = GenerationType.IDENTITY)`. +`WidgetSequence` uses `@GeneratedValue(strategy = GenerationType.SEQUENCE)` with a matching +`allocationSize = 25`. That's the entire diff between the two entity classes. + +## What Hibernate's own statistics say happened + +| | `entityInsertCount` | `prepareStatementCount` | +|---|---|---| +| `WidgetIdentity` (IDENTITY) | 30 | **30** | +| `WidgetSequence` (SEQUENCE) | 30 | **4** | + +`hibernate.jdbc.batch_size=25` did nothing at all for the `IDENTITY` run — every one of the 30 +inserts is its own round trip to the database (`prepareStatementCount` equals +`entityInsertCount`). With `SEQUENCE`, Hibernate knows the id before the row is written, so it +can queue inserts and batch them: 30 rows at a batch size of 25 means two insert batches +(25 + 5), plus two calls to pull the next block of ids from `widget_seq` (the sequence's +`allocationSize` is also 25, so the first 25 ids come from one call and the remaining 5 force a +second) — four prepared statements total, for the same 30 rows. + +## What surprised me building this + +The number that surprised me was not "IDENTITY doesn't batch" — that's documented, if you know +to look for it. It was seeing `prepareStatementCount` for `SEQUENCE` land at exactly **4**, not +2. It's obvious in hindsight — `allocationSize` governs how often the sequence itself gets hit, +independently of `batch_size` governing how the inserts get grouped — but "obvious in hindsight" +and "what I would have guessed beforehand" are different things, and the gap between them is +exactly what running this instead of describing it catches. If `allocationSize` had been left at +JPA's default of `50` instead of matching `batch_size` at `25`, the sequence would only need one +call for all 30 ids, and `prepareStatementCount` would drop to 3 — a change to a number that has +nothing to do with batching, moving a number that looks like it's entirely about batching. + +The practical version of this: if a switch to `SEQUENCE` doesn't produce the batching win the +Hibernate docs promise, checking `hibernate.generate_statistics=true` and reading +`prepareStatementCount` directly answers "is it actually batching" in a way that reading the +`hibernate.jdbc.batch_size` value in a config file cannot — the config says what was requested, +not what happened. + +## Long-tail edge cases not covered above + +- **`save()` vs `persist()`** — `save()` is Hibernate's own pre-JPA API and still works, but + returns the generated id immediately rather than `void`, and can be called outside a + transaction (where it will fail later, confusingly, at flush time). `persist()` is the + JPA-portable choice; there's no scenario for this in this repo because the observable + difference is in the method signature and portability, not in captured runtime behaviour. +- **Bulk inserts via `StatelessSession`** — bypasses the persistence context and lifecycle + callbacks entirely; worth a dedicated repository of its own rather than a profile bolted onto + this one, since the interesting failure modes (cascades silently not firing, no dirty + checking) need a scenario built around triggering them specifically. +- **Native SQL batch inserts via `createNativeQuery` + `addBatch`** — sidesteps Hibernate's own + batching machinery altogether; the batching behaviour at that point is entirely the JDBC + driver's, not Hibernate's, which is a different post. + +## Full captured transcripts + +### `insert-identity.txt` + +```console +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +--- inserting 30 WidgetIdentity rows (GenerationType.IDENTITY) --- +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-1] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-2] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-3] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-4] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-5] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-6] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-7] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-8] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-9] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-10] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-11] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-12] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-13] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-14] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-15] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-16] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-17] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-18] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-19] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-20] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-21] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-22] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-23] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-24] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-25] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-26] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-27] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-28] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-29] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-30] +entityInsertCount = 30 +prepareStatementCount = 30 +(with IDENTITY, expect prepareStatementCount to land close to entityInsertCount -- each insert has to go to the database immediately to hand back the generated key, so there is nothing left for hibernate.jdbc.batch_size to batch) +``` + +### `insert-sequence.txt` + +```console +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +--- inserting 30 WidgetSequence rows (GenerationType.SEQUENCE, allocationSize=25) --- +Hibernate: select next value for widget_seq +Hibernate: select next value for widget_seq +Hibernate: select next value for widget_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-1] +binding parameter (2:BIGINT) <- [1] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-2] +binding parameter (2:BIGINT) <- [2] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-3] +binding parameter (2:BIGINT) <- [3] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-4] +binding parameter (2:BIGINT) <- [4] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-5] +binding parameter (2:BIGINT) <- [5] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-6] +binding parameter (2:BIGINT) <- [6] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-7] +binding parameter (2:BIGINT) <- [7] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-8] +binding parameter (2:BIGINT) <- [8] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-9] +binding parameter (2:BIGINT) <- [9] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-10] +binding parameter (2:BIGINT) <- [10] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-11] +binding parameter (2:BIGINT) <- [11] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-12] +binding parameter (2:BIGINT) <- [12] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-13] +binding parameter (2:BIGINT) <- [13] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-14] +binding parameter (2:BIGINT) <- [14] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-15] +binding parameter (2:BIGINT) <- [15] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-16] +binding parameter (2:BIGINT) <- [16] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-17] +binding parameter (2:BIGINT) <- [17] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-18] +binding parameter (2:BIGINT) <- [18] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-19] +binding parameter (2:BIGINT) <- [19] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-20] +binding parameter (2:BIGINT) <- [20] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-21] +binding parameter (2:BIGINT) <- [21] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-22] +binding parameter (2:BIGINT) <- [22] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-23] +binding parameter (2:BIGINT) <- [23] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-24] +binding parameter (2:BIGINT) <- [24] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-25] +binding parameter (2:BIGINT) <- [25] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-26] +binding parameter (2:BIGINT) <- [26] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-27] +binding parameter (2:BIGINT) <- [27] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-28] +binding parameter (2:BIGINT) <- [28] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-29] +binding parameter (2:BIGINT) <- [29] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-30] +binding parameter (2:BIGINT) <- [30] +entityInsertCount = 30 +prepareStatementCount = 4 +(with SEQUENCE, the id is known before the row is written, so Hibernate can defer and batch the inserts -- expect prepareStatementCount well below entityInsertCount) +``` + +[← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md) diff --git a/docs/output/get-vs-load.txt b/docs/output/get-vs-load.txt new file mode 100644 index 0000000..2938941 --- /dev/null +++ b/docs/output/get-vs-load.txt @@ -0,0 +1,46 @@ +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +Hibernate: select next value for book_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,title,version,id) values (?,?,?,?) +binding parameter (1:VARCHAR) <- [Joshua Bloch] +binding parameter (2:VARCHAR) <- [Effective Java] +binding parameter (3:BIGINT) <- [0] +binding parameter (4:BIGINT) <- [1] +SEED: inserted Book id=1 +--- Step 1: session.get() on an existing id --- +about to call session.get(Book.class, 1) +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +get() returned: Book{id=1, title=Effective Java, author=Joshua Bloch, version=0} +--- Step 2: session.get() on a missing id --- +about to call session.get(Book.class, 999001) +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [999001] +get() returned: null (no exception thrown) +--- Step 3: session.getReference() on an existing id --- +getReference() returned proxy of class com.ankurm.hibernatedemo.model.Book$HibernateProxy -- no SELECT above this line +now calling proxy.getTitle() ... +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +getTitle() returned 'Effective Java' -- the SELECT for this ran just above this line +--- Step 4: session.getReference() on a missing id --- +getReference() returned a proxy for a row that does not exist -- no exception yet: com.ankurm.hibernatedemo.model.Book$HibernateProxy +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [999001] +accessing the proxy threw jakarta.persistence.EntityNotFoundException: No row with the given identifier exists for entity [com.ankurm.hibernatedemo.model.Book with id '999001'] +--- Step 5: proxy accessed after its session is closed --- +session closed. proxy in hand: com.ankurm.hibernatedemo.model.Book$HibernateProxy +accessing the proxy after close threw org.hibernate.LazyInitializationException: Could not initialize proxy [com.ankurm.hibernatedemo.model.Book#1] - no session +--- Step 6: proxy identity vs a real loaded instance --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +real.getClass() = com.ankurm.hibernatedemo.model.Book +proxy.getClass() = com.ankurm.hibernatedemo.model.Book$HibernateProxy +proxy instanceof Book.class: true +real.getClass() == proxy.getClass(): false +real.equals(proxy) before proxy access: false diff --git a/docs/output/insert-identity.txt b/docs/output/insert-identity.txt new file mode 100644 index 0000000..434dae5 --- /dev/null +++ b/docs/output/insert-identity.txt @@ -0,0 +1,71 @@ +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +--- inserting 30 WidgetIdentity rows (GenerationType.IDENTITY) --- +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-1] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-2] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-3] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-4] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-5] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-6] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-7] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-8] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-9] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-10] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-11] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-12] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-13] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-14] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-15] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-16] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-17] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-18] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-19] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-20] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-21] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-22] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-23] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-24] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-25] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-26] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-27] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-28] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-29] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetIdentity */insert into widget_identity (name,id) values (?,default) +binding parameter (1:VARCHAR) <- [identity-30] +entityInsertCount = 30 +prepareStatementCount = 30 +(with IDENTITY, expect prepareStatementCount to land close to entityInsertCount -- each insert has to go to the database immediately to hand back the generated key, so there is nothing left for hibernate.jdbc.batch_size to batch) diff --git a/docs/output/insert-sequence.txt b/docs/output/insert-sequence.txt new file mode 100644 index 0000000..5971a11 --- /dev/null +++ b/docs/output/insert-sequence.txt @@ -0,0 +1,104 @@ +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +--- inserting 30 WidgetSequence rows (GenerationType.SEQUENCE, allocationSize=25) --- +Hibernate: select next value for widget_seq +Hibernate: select next value for widget_seq +Hibernate: select next value for widget_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-1] +binding parameter (2:BIGINT) <- [1] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-2] +binding parameter (2:BIGINT) <- [2] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-3] +binding parameter (2:BIGINT) <- [3] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-4] +binding parameter (2:BIGINT) <- [4] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-5] +binding parameter (2:BIGINT) <- [5] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-6] +binding parameter (2:BIGINT) <- [6] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-7] +binding parameter (2:BIGINT) <- [7] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-8] +binding parameter (2:BIGINT) <- [8] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-9] +binding parameter (2:BIGINT) <- [9] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-10] +binding parameter (2:BIGINT) <- [10] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-11] +binding parameter (2:BIGINT) <- [11] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-12] +binding parameter (2:BIGINT) <- [12] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-13] +binding parameter (2:BIGINT) <- [13] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-14] +binding parameter (2:BIGINT) <- [14] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-15] +binding parameter (2:BIGINT) <- [15] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-16] +binding parameter (2:BIGINT) <- [16] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-17] +binding parameter (2:BIGINT) <- [17] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-18] +binding parameter (2:BIGINT) <- [18] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-19] +binding parameter (2:BIGINT) <- [19] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-20] +binding parameter (2:BIGINT) <- [20] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-21] +binding parameter (2:BIGINT) <- [21] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-22] +binding parameter (2:BIGINT) <- [22] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-23] +binding parameter (2:BIGINT) <- [23] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-24] +binding parameter (2:BIGINT) <- [24] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-25] +binding parameter (2:BIGINT) <- [25] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-26] +binding parameter (2:BIGINT) <- [26] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-27] +binding parameter (2:BIGINT) <- [27] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-28] +binding parameter (2:BIGINT) <- [28] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-29] +binding parameter (2:BIGINT) <- [29] +Hibernate: /* insert for com.ankurm.hibernatedemo.model.WidgetSequence */insert into widget_sequence (name,id) values (?,?) +binding parameter (1:VARCHAR) <- [sequence-30] +binding parameter (2:BIGINT) <- [30] +entityInsertCount = 30 +prepareStatementCount = 4 +(with SEQUENCE, the id is known before the row is written, so Hibernate can defer and batch the inserts -- expect prepareStatementCount well below entityInsertCount) diff --git a/docs/output/merge-vs-refresh.txt b/docs/output/merge-vs-refresh.txt new file mode 100644 index 0000000..60112a0 --- /dev/null +++ b/docs/output/merge-vs-refresh.txt @@ -0,0 +1,41 @@ +Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional +Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional +Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id)) +Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id)) +Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id)) +Hibernate: create sequence book_seq start with 1 increment by 50 +Hibernate: create sequence widget_seq start with 1 increment by 25 +Hibernate: select next value for book_seq +Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,title,version,id) values (?,?,?,?) +binding parameter (1:VARCHAR) <- [Robert C. Martin] +binding parameter (2:VARCHAR) <- [Clean Code] +binding parameter (3:BIGINT) <- [0] +binding parameter (4:BIGINT) <- [1] +SEED: inserted Book{id=1, title=Clean Code, author=Robert C. Martin, version=0} +--- Step 1: load the row, then close the session (entity is now detached) --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +detached instance in hand: Book{id=1, title=Clean Code, author=Robert C. Martin, version=0} +--- Step 2: a second, independent session edits the same row and commits --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +Hibernate: /* update for com.ankurm.hibernatedemo.model.Book */update book set author=?,title=?,version=? where id=? and version=? +binding parameter (1:VARCHAR) <- [Robert C. Martin] +binding parameter (2:VARCHAR) <- [Clean Code (2nd Edition)] +binding parameter (3:BIGINT) <- [1] +binding parameter (4:BIGINT) <- [1] +binding parameter (5:BIGINT) <- [0] +second session committed: Book{id=1, title=Clean Code (2nd Edition), author=Robert C. Martin, version=1} -- version column has now advanced in the database +--- Step 3: mutate the ORIGINAL detached instance (still holding the OLD version) and merge() it --- +detached instance before merge (note the version and title are both stale): Book{id=1, title=Clean Code, author=Robert C. Martin (Uncle Bob), version=0} +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +merge() threw jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.ankurm.hibernatedemo.model.Book with id '1'] +the title change from Step 2 survives untouched -- merge() refused to apply a write built on a stale version +--- Step 4: refresh() on a MANAGED entity with an unflushed local edit --- +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +before refresh(): Book{id=1, title=Clean Code (2nd Edition), author=SOMEONE ELSE ENTIRELY (never flushed), version=1} +Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=? +binding parameter (1:BIGINT) <- [1] +after refresh(): Book{id=1, title=Clean Code (2nd Edition), author=Robert C. Martin, version=1} -- the local edit is gone, no exception was thrown diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..a5b7998 --- /dev/null +++ b/pom.xml @@ -0,0 +1,66 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 4.1.0 + + + + com.ankurm + hibernate-demo + 1.0.0 + hibernate-demo + + Companion repository for the ankurm.com Hibernate 7 batch: get() vs load(), merge() vs + refresh(), and inserting objects efficiently. + + + + 25 + + 7.4.1.Final + + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.h2database + h2 + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/scripts/clean_output.py b/scripts/clean_output.py new file mode 100755 index 0000000..5834a1d --- /dev/null +++ b/scripts/clean_output.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Turn a raw mvn spring-boot:run capture into a clean, reproducible transcript. + +Strips JVM/build noise (sun.misc.Unsafe warnings, JAVA_TOOL_OPTIONS proxy banners) and the +duplicate un-prefixed echo of each SQL statement that org.hibernate.SQL's show_sql=true prints +to stdout in addition to the "Hibernate: ..." line the logger emits -- same text twice, so only +the logger-prefixed copy is kept. +""" +import re +import sys + +DROP_PREFIXES = ( + "WARNING:", + "Picked up JAVA_TOOL_OPTIONS", +) + + +def clean(lines): + out = [] + for line in lines: + stripped = line.rstrip("\n") + if any(stripped.startswith(p) for p in DROP_PREFIXES): + continue + # Drop the bare SQL echo line that show_sql=true prints without the "Hibernate: " prefix + # -- it is always immediately followed by the same text WITH the prefix. + out.append(stripped) + deduped = [] + i = 0 + while i < len(out): + cur = out[i] + nxt = out[i + 1] if i + 1 < len(out) else None + if nxt is not None and nxt == "Hibernate: " + cur: + i += 1 # skip the bare echo, keep the prefixed one on the next iteration + continue + deduped.append(out[i]) + i += 1 + return deduped + + +if __name__ == "__main__": + src, dst = sys.argv[1], sys.argv[2] + with open(src, encoding="utf-8") as f: + lines = f.readlines() + cleaned = clean(lines) + with open(dst, "w", encoding="utf-8") as f: + f.write("\n".join(cleaned) + "\n") + print(f"{src}: {len(lines)} -> {dst}: {len(cleaned)} lines") diff --git a/scripts/run-all.sh b/scripts/run-all.sh new file mode 100755 index 0000000..78b1fc9 --- /dev/null +++ b/scripts/run-all.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# Regenerate every file in docs/output/ from a real run. This is the script referenced by +# "regenerated by one command" in the post and the README -- if it stops producing the same +# shape of output, the docs are wrong until it's fixed, not the other way around. +set -eu + +cd "$(dirname "$0")/.." +RAW_DIR="$(mktemp -d)" +trap 'rm -rf "$RAW_DIR"' EXIT + +run_one() { + local profile="$1" outfile="$2" + echo "=== running profile: $profile ===" >&2 + mvn -q -B org.springframework.boot:spring-boot-maven-plugin:run \ + -Dspring-boot.run.profiles="$profile" 2>&1 \ + | grep -v '^Picked up JAVA_TOOL_OPTIONS' \ + | grep -v '^WARNING:' \ + > "$RAW_DIR/$profile.raw.txt" + python3 scripts/clean_output.py "$RAW_DIR/$profile.raw.txt" "docs/output/$outfile" +} + +run_one getvsload get-vs-load.txt +run_one mergerefresh merge-vs-refresh.txt +run_one insert-identity insert-identity.txt +run_one insert-sequence insert-sequence.txt + +echo "docs/output/ regenerated." >&2 diff --git a/scripts/run.sh b/scripts/run.sh new file mode 100755 index 0000000..bac59f9 --- /dev/null +++ b/scripts/run.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Run one scenario in the foreground and exit. +# +# ./scripts/run.sh getvsload +# ./scripts/run.sh mergerefresh +# ./scripts/run.sh insert-identity +# ./scripts/run.sh insert-sequence +# +# Every scenario here is a CommandLineRunner against an in-memory H2 database with +# spring.main.web-application-type=none, so there is no server to keep alive and nothing to +# kill afterwards -- the process runs the scenario and exits on its own. +set -eu + +PROFILE="${1:?usage: run.sh }" + +cd "$(dirname "$0")/.." +mvn -q -B org.springframework.boot:spring-boot-maven-plugin:run \ + -Dspring-boot.run.profiles="$PROFILE" diff --git a/src/main/java/com/ankurm/hibernatedemo/HibernateDemoApplication.java b/src/main/java/com/ankurm/hibernatedemo/HibernateDemoApplication.java new file mode 100644 index 0000000..ec20c8e --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/HibernateDemoApplication.java @@ -0,0 +1,27 @@ +package com.ankurm.hibernatedemo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Entry point for the companion demos behind three ankurm.com Hibernate 7 posts: + * + *
    + *
  • {@code get-vs-load} — docs/01-get-vs-load.md, {@link com.ankurm.hibernatedemo.scenario.GetVsLoadRunner}
  • + *
  • {@code merge-vs-refresh} — docs/02-merge-vs-refresh.md, {@link com.ankurm.hibernatedemo.scenario.MergeVsRefreshRunner}
  • + *
  • {@code insert-identity} / {@code insert-sequence} — docs/03-inserting-objects.md, + * {@link com.ankurm.hibernatedemo.scenario.InsertIdentityRunner} and + * {@link com.ankurm.hibernatedemo.scenario.InsertSequenceRunner}
  • + *
+ * + * Each scenario is a profile-gated {@link org.springframework.boot.CommandLineRunner} that runs + * once against an in-memory H2 database and exits — there is no web server to keep alive, + * so {@code scripts/run.sh <profile>} is a plain foreground {@code mvn spring-boot:run} call. + */ +@SpringBootApplication +public class HibernateDemoApplication { + + public static void main(String[] args) { + SpringApplication.run(HibernateDemoApplication.class, args); + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/model/Book.java b/src/main/java/com/ankurm/hibernatedemo/model/Book.java new file mode 100644 index 0000000..1a89cdc --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/model/Book.java @@ -0,0 +1,69 @@ +package com.ankurm.hibernatedemo.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +/** + * The entity used by both {@code get-vs-load} and {@code merge-vs-refresh}. + * + *

Docs: docs/01-get-vs-load.md, docs/02-merge-vs-refresh.md. + * + *

Carries a {@code @Version} column on purpose — the merge/refresh scenario needs a + * real optimistic-lock field to show what merge() does when the version it is holding is stale, + * not just what it does to a plain column. + */ +@Entity +public class Book { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq") + private Long id; + + private String title; + + private String author; + + @Version + private Long version; + + protected Book() { + // JPA + } + + public Book(String title, String author) { + this.title = title; + this.author = author; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String author) { + this.author = author; + } + + public Long getVersion() { + return version; + } + + @Override + public String toString() { + return "Book{id=%s, title=%s, author=%s, version=%s}".formatted(id, title, author, version); + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java b/src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java new file mode 100644 index 0000000..650d4dc --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java @@ -0,0 +1,41 @@ +package com.ankurm.hibernatedemo.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; + +/** + * Insert-scenario twin of {@link WidgetSequence}, identical except for the id generation + * strategy. Docs: docs/03-inserting-objects.md. + * + *

{@code IDENTITY} requires the database to hand back the generated key on every single + * insert, which is exactly why it defeats JDBC batching — see the captured output in + * docs/output/insert-identity.txt versus docs/output/insert-sequence.txt for the same + * {@code hibernate.jdbc.batch_size} setting producing very different behaviour. + */ +@Entity +public class WidgetIdentity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + protected WidgetIdentity() { + // JPA + } + + public WidgetIdentity(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java b/src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java new file mode 100644 index 0000000..9cbaf30 --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java @@ -0,0 +1,42 @@ +package com.ankurm.hibernatedemo.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; + +/** + * Insert-scenario twin of {@link WidgetIdentity}. Docs: docs/03-inserting-objects.md. + * + *

{@code allocationSize} matches {@code hibernate.jdbc.batch_size} in + * {@code application-insert-sequence.yml} on purpose: a mismatched allocation size is its own + * classic footgun (extra round trips to refill the sequence pool mid-batch) and not one this + * repo is trying to demonstrate here. + */ +@Entity +public class WidgetSequence { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_seq") + @SequenceGenerator(name = "widget_seq", sequenceName = "widget_seq", allocationSize = 25) + private Long id; + + private String name; + + protected WidgetSequence() { + // JPA + } + + public WidgetSequence(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java b/src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java new file mode 100644 index 0000000..e9de341 --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java @@ -0,0 +1,154 @@ +package com.ankurm.hibernatedemo.scenario; + +import com.ankurm.hibernatedemo.model.Book; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Backs ankurm.com post 4859 ("Hibernate 7: get() vs load()") and + * docs/01-get-vs-load.md. Captured verbatim into docs/output/get-vs-load.txt by + * {@code scripts/run.sh getvsload}. + * + *

Each step opens its own {@link EntityManager} deliberately, so the SQL log lines that + * bracket a step are unambiguously that step's own traffic — there is no shared session + * whose first-level cache could quietly answer a later {@code get()} for free. + */ +@Component +@Profile("getvsload") +public class GetVsLoadRunner implements CommandLineRunner { + + private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); + + private final EntityManagerFactory emf; + + public GetVsLoadRunner(EntityManagerFactory emf) { + this.emf = emf; + } + + @Override + public void run(String... args) { + Long existingId = seedOneBook(); + long missingId = existingId + 999_000L; + + step1_getExisting(existingId); + step2_getMissing(missingId); + step3_getReferenceExisting_noSelectUntilAccessed(existingId); + step4_getReferenceMissing_exceptionOnlyOnAccess(missingId); + step5_getReferenceThenSessionClosed_lazyInitException(existingId); + step6_proxyVsRealIdentity(existingId); + } + + private Long seedOneBook() { + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Book book = new Book("Effective Java", "Joshua Bloch"); + em.persist(book); + em.getTransaction().commit(); + Long id = book.getId(); + em.close(); + DEMO.info("SEED: inserted Book id={}", id); + return id; + } + + private void step1_getExisting(Long id) { + DEMO.info("--- Step 1: session.get() on an existing id ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + DEMO.info("about to call session.get(Book.class, {})", id); + Book book = session.get(Book.class, id); + DEMO.info("get() returned: {}", book); + em.getTransaction().commit(); + em.close(); + } + + private void step2_getMissing(long missingId) { + DEMO.info("--- Step 2: session.get() on a missing id ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + DEMO.info("about to call session.get(Book.class, {})", missingId); + Book book = session.get(Book.class, missingId); + DEMO.info("get() returned: {} (no exception thrown)", book); + em.getTransaction().commit(); + em.close(); + } + + private void step3_getReferenceExisting_noSelectUntilAccessed(Long id) { + DEMO.info("--- Step 3: session.getReference() on an existing id ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book proxy = session.getReference(Book.class, id); + DEMO.info("getReference() returned proxy of class {} -- no SELECT above this line", proxy.getClass().getName()); + DEMO.info("now calling proxy.getTitle() ..."); + String title = proxy.getTitle(); + DEMO.info("getTitle() returned '{}' -- the SELECT for this ran just above this line", title); + em.getTransaction().commit(); + em.close(); + } + + private void step4_getReferenceMissing_exceptionOnlyOnAccess(long missingId) { + DEMO.info("--- Step 4: session.getReference() on a missing id ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book proxy = session.getReference(Book.class, missingId); + DEMO.info("getReference() returned a proxy for a row that does not exist -- no exception yet: {}", proxy.getClass().getName()); + try { + proxy.getTitle(); + DEMO.info("no exception -- this line should be unreachable"); + } catch (RuntimeException e) { + DEMO.info("accessing the proxy threw {}: {}", e.getClass().getName(), e.getMessage()); + } + em.getTransaction().rollback(); + em.close(); + } + + private void step5_getReferenceThenSessionClosed_lazyInitException(Long id) { + DEMO.info("--- Step 5: proxy accessed after its session is closed ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book proxy = session.getReference(Book.class, id); + em.getTransaction().commit(); + em.close(); + DEMO.info("session closed. proxy in hand: {}", proxy.getClass().getName()); + try { + proxy.getTitle(); + DEMO.info("no exception -- this line should be unreachable"); + } catch (RuntimeException e) { + DEMO.info("accessing the proxy after close threw {}: {}", e.getClass().getName(), e.getMessage()); + } + } + + private void step6_proxyVsRealIdentity(Long id) { + DEMO.info("--- Step 6: proxy identity vs a real loaded instance ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book real = session.get(Book.class, id); + // second em/session so this is a genuinely separate proxy, not the same cached instance + EntityManager em2 = emf.createEntityManager(); + em2.getTransaction().begin(); + Session session2 = em2.unwrap(Session.class); + Book proxy = session2.getReference(Book.class, id); + + DEMO.info("real.getClass() = {}", real.getClass().getName()); + DEMO.info("proxy.getClass() = {}", proxy.getClass().getName()); + DEMO.info("proxy instanceof Book.class: {}", Book.class.isInstance(proxy)); + DEMO.info("real.getClass() == proxy.getClass(): {}", real.getClass() == proxy.getClass()); + DEMO.info("real.equals(proxy) before proxy access: {}", real.equals(proxy)); + + em.getTransaction().commit(); + em.close(); + em2.getTransaction().commit(); + em2.close(); + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java b/src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java new file mode 100644 index 0000000..91e05a6 --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/scenario/InsertIdentityRunner.java @@ -0,0 +1,58 @@ +package com.ankurm.hibernatedemo.scenario; + +import com.ankurm.hibernatedemo.model.WidgetIdentity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Backs ankurm.com post 4861 ("inserting objects efficiently") and + * docs/03-inserting-objects.md. Captured verbatim into docs/output/insert-identity.txt by + * {@code scripts/run.sh insert-identity}. + * + *

Same {@code hibernate.jdbc.batch_size} and {@code hibernate.order_inserts} settings as + * {@link InsertSequenceRunner} -- the only difference is {@link WidgetIdentity}'s + * {@code GenerationType.IDENTITY} strategy. Compare the two captured output files directly; + * the diff between them is the entire point of this pair. + */ +@Component +@Profile("insert-identity") +public class InsertIdentityRunner implements CommandLineRunner { + + private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); + private static final int ROW_COUNT = 30; + + private final EntityManagerFactory emf; + + public InsertIdentityRunner(EntityManagerFactory emf) { + this.emf = emf; + } + + @Override + public void run(String... args) { + SessionFactory sessionFactory = emf.unwrap(SessionFactory.class); + Statistics stats = sessionFactory.getStatistics(); + stats.clear(); + + DEMO.info("--- inserting {} WidgetIdentity rows (GenerationType.IDENTITY) ---", ROW_COUNT); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + for (int i = 1; i <= ROW_COUNT; i++) { + em.persist(new WidgetIdentity("identity-" + i)); + } + em.getTransaction().commit(); + em.close(); + + DEMO.info("entityInsertCount = {}", stats.getEntityInsertCount()); + DEMO.info("prepareStatementCount = {}", stats.getPrepareStatementCount()); + DEMO.info("(with IDENTITY, expect prepareStatementCount to land close to entityInsertCount --" + + " each insert has to go to the database immediately to hand back the generated key," + + " so there is nothing left for hibernate.jdbc.batch_size to batch)"); + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java b/src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java new file mode 100644 index 0000000..f0c07c9 --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/scenario/InsertSequenceRunner.java @@ -0,0 +1,55 @@ +package com.ankurm.hibernatedemo.scenario; + +import com.ankurm.hibernatedemo.model.WidgetSequence; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.hibernate.SessionFactory; +import org.hibernate.stat.Statistics; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Backs ankurm.com post 4861 ("inserting objects efficiently") and + * docs/03-inserting-objects.md. Captured verbatim into docs/output/insert-sequence.txt by + * {@code scripts/run.sh insert-sequence}. + * + *

Same {@code hibernate.jdbc.batch_size} and {@code hibernate.order_inserts} settings as + * {@link InsertIdentityRunner} -- see that class's Javadoc for what this pair is demonstrating. + */ +@Component +@Profile("insert-sequence") +public class InsertSequenceRunner implements CommandLineRunner { + + private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); + private static final int ROW_COUNT = 30; + + private final EntityManagerFactory emf; + + public InsertSequenceRunner(EntityManagerFactory emf) { + this.emf = emf; + } + + @Override + public void run(String... args) { + SessionFactory sessionFactory = emf.unwrap(SessionFactory.class); + Statistics stats = sessionFactory.getStatistics(); + stats.clear(); + + DEMO.info("--- inserting {} WidgetSequence rows (GenerationType.SEQUENCE, allocationSize=25) ---", ROW_COUNT); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + for (int i = 1; i <= ROW_COUNT; i++) { + em.persist(new WidgetSequence("sequence-" + i)); + } + em.getTransaction().commit(); + em.close(); + + DEMO.info("entityInsertCount = {}", stats.getEntityInsertCount()); + DEMO.info("prepareStatementCount = {}", stats.getPrepareStatementCount()); + DEMO.info("(with SEQUENCE, the id is known before the row is written, so Hibernate can" + + " defer and batch the inserts -- expect prepareStatementCount well below entityInsertCount)"); + } +} diff --git a/src/main/java/com/ankurm/hibernatedemo/scenario/MergeVsRefreshRunner.java b/src/main/java/com/ankurm/hibernatedemo/scenario/MergeVsRefreshRunner.java new file mode 100644 index 0000000..0e32e49 --- /dev/null +++ b/src/main/java/com/ankurm/hibernatedemo/scenario/MergeVsRefreshRunner.java @@ -0,0 +1,113 @@ +package com.ankurm.hibernatedemo.scenario; + +import com.ankurm.hibernatedemo.model.Book; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.OptimisticLockException; +import org.hibernate.Session; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +/** + * Backs ankurm.com post 4860 ("merge() vs refresh()") and docs/02-merge-vs-refresh.md. + * Captured verbatim into docs/output/merge-vs-refresh.txt by + * {@code scripts/run.sh mergerefresh}. + * + *

{@link Book} carries a {@code @Version} column specifically so this scenario can show what + * {@code merge()} does when the detached instance it is given is holding a version older than + * what is currently in the database — not just what it does to an un-versioned row. + */ +@Component +@Profile("mergerefresh") +public class MergeVsRefreshRunner implements CommandLineRunner { + + private static final Logger DEMO = LoggerFactory.getLogger("DEMO"); + + private final EntityManagerFactory emf; + + public MergeVsRefreshRunner(EntityManagerFactory emf) { + this.emf = emf; + } + + @Override + public void run(String... args) { + Long id = seedOneBook(); + Book detached = loadThenDetach(id); + simulateAnotherProcessEditingTheRow(id); + mergeStaleDetachedInstance(detached); + refreshSilentlyDiscardsUnflushedEdit(id); + } + + private Long seedOneBook() { + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Book book = new Book("Clean Code", "Robert C. Martin"); + em.persist(book); + em.getTransaction().commit(); + Long id = book.getId(); + em.close(); + DEMO.info("SEED: inserted {}", book); + return id; + } + + private Book loadThenDetach(Long id) { + DEMO.info("--- Step 1: load the row, then close the session (entity is now detached) ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book book = session.get(Book.class, id); + em.getTransaction().commit(); + em.close(); + DEMO.info("detached instance in hand: {}", book); + return book; + } + + private void simulateAnotherProcessEditingTheRow(Long id) { + DEMO.info("--- Step 2: a second, independent session edits the same row and commits ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book book = session.get(Book.class, id); + book.setTitle("Clean Code (2nd Edition)"); + em.getTransaction().commit(); + em.close(); + DEMO.info("second session committed: {} -- version column has now advanced in the database", book); + } + + private void mergeStaleDetachedInstance(Book detached) { + DEMO.info("--- Step 3: mutate the ORIGINAL detached instance (still holding the OLD version) and merge() it ---"); + detached.setAuthor("Robert C. Martin (Uncle Bob)"); + DEMO.info("detached instance before merge (note the version and title are both stale): {}", detached); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + try { + Book merged = session.merge(detached); + em.getTransaction().commit(); + DEMO.info("merge() succeeded, returned managed instance: {}", merged); + } catch (OptimisticLockException e) { + em.getTransaction().rollback(); + DEMO.info("merge() threw {}: {}", e.getClass().getName(), e.getMessage()); + DEMO.info("the title change from Step 2 survives untouched -- merge() refused to apply a write built on a stale version"); + } finally { + em.close(); + } + } + + private void refreshSilentlyDiscardsUnflushedEdit(Long id) { + DEMO.info("--- Step 4: refresh() on a MANAGED entity with an unflushed local edit ---"); + EntityManager em = emf.createEntityManager(); + em.getTransaction().begin(); + Session session = em.unwrap(Session.class); + Book book = session.get(Book.class, id); + book.setAuthor("SOMEONE ELSE ENTIRELY (never flushed)"); + DEMO.info("before refresh(): {}", book); + session.refresh(book); + DEMO.info("after refresh(): {} -- the local edit is gone, no exception was thrown", book); + em.getTransaction().commit(); + em.close(); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..cee1e41 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,34 @@ +spring: + main: + web-application-type: none + banner-mode: off + datasource: + url: jdbc:h2:mem:hibernate-demo;DB_CLOSE_DELAY=-1 + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: update + open-in-view: false + properties: + hibernate: + show_sql: true + format_sql: false + use_sql_comments: true + generate_statistics: true + jdbc: + batch_size: 25 + order_inserts: true + order_updates: true + +logging: + level: + root: WARN + DEMO: INFO + org.hibernate.SQL: DEBUG + org.hibernate.orm.jdbc.bind: TRACE + org.hibernate.engine.jdbc.batch.internal.BatchingBatch: DEBUG + org.hibernate.stat: INFO + pattern: + console: "%msg%n"