1
0

Add hibernate-demo: get() vs load(), merge() vs refresh(), inserting objects (Hibernate 7.4.1.Final + Spring Boot 4.1.0)

This commit is contained in:
2026-08-26 17:26:04 +00:00
commit 5533c6c8fe
24 changed files with 1653 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
target/
*.class
.idea/
*.iml
.vscode/
.DS_Store

21
LICENSE Normal file
View File

@@ -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.

99
README.md Normal file
View File

@@ -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).

45
docs/00-versions.md Normal file
View File

@@ -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 `<hibernate.version>7.4.1.Final</hibernate.version>`
so on Boot 4.1.0, `pom.xml` in this repo does not need to override anything to get 7.4.1.Final;
the `<hibernate.version>` 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 `<hibernate.version>` 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)

98
docs/01-get-vs-load.md Normal file
View File

@@ -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)

108
docs/02-merge-vs-refresh.md Normal file
View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

66
pom.xml Normal file
View File

@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>4.1.0</version>
<relativePath/>
</parent>
<groupId>com.ankurm</groupId>
<artifactId>hibernate-demo</artifactId>
<version>1.0.0</version>
<name>hibernate-demo</name>
<description>
Companion repository for the ankurm.com Hibernate 7 batch: get() vs load(), merge() vs
refresh(), and inserting objects efficiently.
</description>
<properties>
<java.version>25</java.version>
<!--
Spring Boot 4.1.0's own dependency management already resolves hibernate.version to
7.4.1.Final (checked in spring-boot-dependencies-4.1.0.pom before writing a line of code
here). This property is declared anyway, not to override anything, but so the pin is
visible in the POM and survives a future Boot patch release quietly bumping it under us.
See docs/00-versions.md for the two Boot releases (4.1.0 and 4.1.1) that resolve to two
different Hibernate patch versions from the same "4.1" line.
-->
<hibernate.version>7.4.1.Final</hibernate.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

47
scripts/clean_output.py Executable file
View File

@@ -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")

27
scripts/run-all.sh Executable file
View File

@@ -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

18
scripts/run.sh Executable file
View File

@@ -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 <getvsload|mergerefresh|insert-identity|insert-sequence>}"
cd "$(dirname "$0")/.."
mvn -q -B org.springframework.boot:spring-boot-maven-plugin:run \
-Dspring-boot.run.profiles="$PROFILE"

View File

@@ -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:
*
* <ul>
* <li>{@code get-vs-load} &mdash; docs/01-get-vs-load.md, {@link com.ankurm.hibernatedemo.scenario.GetVsLoadRunner}</li>
* <li>{@code merge-vs-refresh} &mdash; docs/02-merge-vs-refresh.md, {@link com.ankurm.hibernatedemo.scenario.MergeVsRefreshRunner}</li>
* <li>{@code insert-identity} / {@code insert-sequence} &mdash; docs/03-inserting-objects.md,
* {@link com.ankurm.hibernatedemo.scenario.InsertIdentityRunner} and
* {@link com.ankurm.hibernatedemo.scenario.InsertSequenceRunner}</li>
* </ul>
*
* Each scenario is a profile-gated {@link org.springframework.boot.CommandLineRunner} that runs
* once against an in-memory H2 database and exits &mdash; there is no web server to keep alive,
* so {@code scripts/run.sh &lt;profile&gt;} 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);
}
}

View File

@@ -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}.
*
* <p>Docs: docs/01-get-vs-load.md, docs/02-merge-vs-refresh.md.
*
* <p>Carries a {@code @Version} column on purpose &mdash; 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);
}
}

View File

@@ -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.
*
* <p>{@code IDENTITY} requires the database to hand back the generated key on every single
* insert, which is exactly why it defeats JDBC batching &mdash; 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;
}
}

View File

@@ -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.
*
* <p>{@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;
}
}

View File

@@ -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}.
*
* <p>Each step opens its own {@link EntityManager} deliberately, so the SQL log lines that
* bracket a step are unambiguously that step's own traffic &mdash; 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();
}
}

View File

@@ -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}.
*
* <p>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)");
}
}

View File

@@ -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}.
*
* <p>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)");
}
}

View File

@@ -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}.
*
* <p>{@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 &mdash; 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();
}
}

View File

@@ -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"