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:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
target/
|
||||
*.class
|
||||
.idea/
|
||||
*.iml
|
||||
.vscode/
|
||||
.DS_Store
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal 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
99
README.md
Normal 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
45
docs/00-versions.md
Normal 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
98
docs/01-get-vs-load.md
Normal 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
108
docs/02-merge-vs-refresh.md
Normal 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)
|
||||
263
docs/03-inserting-objects.md
Normal file
263
docs/03-inserting-objects.md
Normal 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)
|
||||
46
docs/output/get-vs-load.txt
Normal file
46
docs/output/get-vs-load.txt
Normal 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
|
||||
71
docs/output/insert-identity.txt
Normal file
71
docs/output/insert-identity.txt
Normal 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)
|
||||
104
docs/output/insert-sequence.txt
Normal file
104
docs/output/insert-sequence.txt
Normal 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)
|
||||
41
docs/output/merge-vs-refresh.txt
Normal file
41
docs/output/merge-vs-refresh.txt
Normal 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
66
pom.xml
Normal 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
47
scripts/clean_output.py
Executable 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
27
scripts/run-all.sh
Executable 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
18
scripts/run.sh
Executable 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"
|
||||
@@ -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} — docs/01-get-vs-load.md, {@link com.ankurm.hibernatedemo.scenario.GetVsLoadRunner}</li>
|
||||
* <li>{@code merge-vs-refresh} — docs/02-merge-vs-refresh.md, {@link com.ankurm.hibernatedemo.scenario.MergeVsRefreshRunner}</li>
|
||||
* <li>{@code insert-identity} / {@code insert-sequence} — 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 — 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);
|
||||
}
|
||||
}
|
||||
69
src/main/java/com/ankurm/hibernatedemo/model/Book.java
Normal file
69
src/main/java/com/ankurm/hibernatedemo/model/Book.java
Normal 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 — 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);
|
||||
}
|
||||
}
|
||||
@@ -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 — 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 — 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();
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
@@ -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)");
|
||||
}
|
||||
}
|
||||
@@ -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 — 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();
|
||||
}
|
||||
}
|
||||
34
src/main/resources/application.yml
Normal file
34
src/main/resources/application.yml
Normal 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"
|
||||
Reference in New Issue
Block a user