Add hibernate-demo: get() vs load(), merge() vs refresh(), inserting objects (Hibernate 7.4.1.Final + Spring Boot 4.1.0)
Adds a JUnit test suite (GetVsGetReferenceTest, MergeRefreshTest, OptimisticLockTest, IdentityBatchTest, SequenceBatchTest, AllocationSizeSweepTest, BatchSizeSweepTest) so every surprising behavior described in the three companion posts has a reproducible test, alongside the original CommandLineRunner scenarios. Rewrites all three doc chapters and the README around the new experiments: the get()/getReference() same-session matrix, the merge()/refresh() experiments (including exactly when OptimisticLockException surfaces and a corrected LAZY-plus- cascade merge() result), and two new sweeps (allocationSize, batch_size) for batch inserts.
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.
|
||||
135
README.md
Normal file
135
README.md
Normal file
@@ -0,0 +1,135 @@
|
||||
# hibernate-demo
|
||||
|
||||
Companion repository for three ankurm.com posts on Hibernate 7's persistence-context APIs:
|
||||
`get()` vs `getReference()`, `merge()` vs `refresh()`, and batch inserts. Every claim in those
|
||||
posts that comes from this repo traces to a named JUnit test here and a captured transcript in
|
||||
`docs/output/` — nothing is asserted that wasn't actually run. Every surprising behavior
|
||||
described in the three posts has a reproducible test backing it; the table below maps each one.
|
||||
|
||||
## 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
|
||||
./mvnw test
|
||||
```
|
||||
|
||||
Requires JDK 25 and a network connection the first time (Maven needs to fetch plugins online
|
||||
before `-o` offline mode works for subsequent runs).
|
||||
|
||||
## Every surprising claim, mapped to a test
|
||||
|
||||
Each row is one `./mvnw -Dtest=ClassName test` away from reproducing itself. This is the
|
||||
reproduction path referenced throughout all three posts and all three doc chapters below.
|
||||
|
||||
| Test class | Backs post | Proves |
|
||||
|---|---|---|
|
||||
| [`GetVsGetReferenceTest`](src/test/java/com/ankurm/hibernatedemo/GetVsGetReferenceTest.java) | 4859 (get vs getReference) | The 4-calls-4-outcomes table, the same-session matrix, proxy identity vs `equals()` |
|
||||
| [`MergeRefreshTest`](src/test/java/com/ankurm/hibernatedemo/MergeRefreshTest.java) | 4860 (merge vs refresh) | `merge()` returns the pre-existing managed instance; `merge()` initializes a cascaded LAZY collection; `refresh()` silently discards an unflushed edit |
|
||||
| [`OptimisticLockTest`](src/test/java/com/ankurm/hibernatedemo/OptimisticLockTest.java) | 4860 (merge vs refresh) | Exactly when `OptimisticLockException` surfaces relative to `merge()`/commit |
|
||||
| [`IdentityBatchTest`](src/test/java/com/ankurm/hibernatedemo/IdentityBatchTest.java) | 4861 (batch inserts) | `GenerationType.IDENTITY` disables batching entirely |
|
||||
| [`SequenceBatchTest`](src/test/java/com/ankurm/hibernatedemo/SequenceBatchTest.java) | 4861 (batch inserts) | `GenerationType.SEQUENCE` allows real batching |
|
||||
| [`AllocationSizeSweepTest`](src/test/java/com/ankurm/hibernatedemo/AllocationSizeSweepTest.java) | 4861 (batch inserts) | `allocationSize` sweep (1, 10, 25, 50) at fixed `batch_size=25` |
|
||||
| [`BatchSizeSweepTest`](src/test/java/com/ankurm/hibernatedemo/BatchSizeSweepTest.java) | 4861 (batch inserts) | `batch_size` sweep (1, 10, 25, 50) at fixed `allocationSize=50` |
|
||||
|
||||
```bash
|
||||
./mvnw -Dtest=GetVsGetReferenceTest test
|
||||
./mvnw -Dtest=MergeRefreshTest,OptimisticLockTest test
|
||||
./mvnw -Dtest=IdentityBatchTest,SequenceBatchTest test
|
||||
./mvnw -Dtest=AllocationSizeSweepTest test
|
||||
./mvnw -Dtest=BatchSizeSweepTest test
|
||||
./mvnw test # the whole suite, 24 tests, 0 failures as of the last commit
|
||||
```
|
||||
|
||||
## CommandLineRunner scenarios
|
||||
|
||||
The original narrative scenarios are still here, unchanged, for anyone who wants to read a
|
||||
straight-line script instead of a test class:
|
||||
|
||||
| Profile | Runs | Chapter |
|
||||
|---|---|---|
|
||||
| `getvsload` | `session.get()` vs `session.getReference()`, proxies, `LazyInitializationException` | [docs/01-get-vs-load.md](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 the `CommandLineRunner`-scenario files in `docs/output/`. `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. The test-suite transcripts in `docs/output/` (the
|
||||
session matrix, the sweeps) were captured the same way, from `./mvnw -Dtest=... test` piped
|
||||
through the same script.
|
||||
|
||||
## 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 getReference()](docs/01-get-vs-load.md) | The 4-calls-4-outcomes table, the same-session matrix, proxy identity vs `equals()` |
|
||||
| [02 — merge() vs refresh()](docs/02-merge-vs-refresh.md) | Three named experiments: which method fails loudly vs silently, exactly when the optimistic-lock check fires, and what a cascaded LAZY collection does under merge() |
|
||||
| [03 — Hibernate 7 batch inserts](docs/03-inserting-objects.md) | `IDENTITY` vs `SEQUENCE`, the allocationSize and batch_size sweeps, `Session` vs `StatelessSession`, with `hibernate.generate_statistics` as evidence throughout |
|
||||
|
||||
## Captured output index
|
||||
|
||||
| File | Source |
|
||||
|---|---|
|
||||
| [docs/output/get-vs-load.txt](docs/output/get-vs-load.txt) | `./scripts/run.sh getvsload` (CommandLineRunner) |
|
||||
| [docs/output/get-vs-getreference-tests.txt](docs/output/get-vs-getreference-tests.txt) | `./mvnw -Dtest=GetVsGetReferenceTest test` |
|
||||
| [docs/output/merge-vs-refresh.txt](docs/output/merge-vs-refresh.txt) | `./scripts/run.sh mergerefresh` (CommandLineRunner) |
|
||||
| [docs/output/merge-vs-refresh-tests.txt](docs/output/merge-vs-refresh-tests.txt) | `./mvnw -Dtest=MergeRefreshTest,OptimisticLockTest test` |
|
||||
| [docs/output/insert-identity.txt](docs/output/insert-identity.txt) | `./scripts/run.sh insert-identity` (also matches `IdentityBatchTest`) |
|
||||
| [docs/output/insert-sequence.txt](docs/output/insert-sequence.txt) | `./scripts/run.sh insert-sequence` (also matches `SequenceBatchTest`) |
|
||||
| [docs/output/allocation-and-batch-size-sweeps.txt](docs/output/allocation-and-batch-size-sweeps.txt) | `./mvnw -Dtest=AllocationSizeSweepTest,BatchSizeSweepTest test` |
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
hibernate-demo/
|
||||
├── pom.xml
|
||||
├── LICENSE
|
||||
├── scripts/
|
||||
│ ├── run.sh start one CommandLineRunner profile, run it, exit
|
||||
│ ├── run-all.sh regenerate the CommandLineRunner docs/output/ files
|
||||
│ └── clean_output.py strip JVM noise + a duplicate SQL echo line from a raw capture
|
||||
├── src/main/java/com/ankurm/hibernatedemo/
|
||||
│ ├── HibernateDemoApplication.java
|
||||
│ ├── model/ Book, Note, WidgetIdentity, WidgetSequence,
|
||||
│ │ WidgetAlloc1/10/25/50, WidgetBatchSweep1/10/25/50
|
||||
│ └── scenario/ one CommandLineRunner per profile
|
||||
├── src/test/java/com/ankurm/hibernatedemo/
|
||||
│ ├── GetVsGetReferenceTest.java
|
||||
│ ├── MergeRefreshTest.java, OptimisticLockTest.java
|
||||
│ ├── IdentityBatchTest.java, SequenceBatchTest.java
|
||||
│ └── AllocationSizeSweepTest.java, BatchSizeSweepTest.java
|
||||
└── docs/
|
||||
├── 00-versions.md .. 03-inserting-objects.md
|
||||
└── output/*.txt captured, unedited console transcripts (both runner and test output)
|
||||
```
|
||||
|
||||
## 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)
|
||||
134
docs/01-get-vs-load.md
Normal file
134
docs/01-get-vs-load.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# 01 — get() vs 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/).
|
||||
|
||||
Test class: [`GetVsGetReferenceTest`](../src/test/java/com/ankurm/hibernatedemo/GetVsGetReferenceTest.java).
|
||||
Run it yourself:
|
||||
|
||||
```bash
|
||||
git clone https://ankurm.com/git.app/asmhatre/hibernate-demo.git
|
||||
cd hibernate-demo
|
||||
./mvnw -Dtest=GetVsGetReferenceTest test
|
||||
```
|
||||
|
||||
Every number and exception class name below came from that command, not from documentation or
|
||||
memory. Raw captured output: [`docs/output/get-vs-getreference-tests.txt`](output/get-vs-getreference-tests.txt).
|
||||
|
||||
## Contract vs observation
|
||||
|
||||
The JPA/Hibernate contract for these two methods is short: `get()` fetches now and may return
|
||||
`null`; `getReference()` defers and may throw once accessed. That contract is real and both
|
||||
methods honor it. What it doesn't tell you is what happens once the *same id* has already been
|
||||
touched once in the *same session* — and that's where the interesting behavior lives, because it's
|
||||
governed by the persistence context, not by the method you happen to call second.
|
||||
|
||||
## Mental model
|
||||
|
||||
Stop thinking of `get()` vs `getReference()` as "eager vs lazy." Think of it as what you're telling
|
||||
Hibernate you need:
|
||||
|
||||
- `get()` says **"I need the entity."** Hibernate will do whatever it takes — including firing a
|
||||
`SELECT` against an id it already has a reference for — to hand you something with real data
|
||||
behind it.
|
||||
- `getReference()` says **"I need a reference."** Hibernate will hand you the cheapest possible
|
||||
object that satisfies that and defers everything else, including telling you the row doesn't
|
||||
exist.
|
||||
|
||||
That framing predicts the session-matrix results in the next section better than "eager vs lazy"
|
||||
does — see the `getReference()` → `get()` row in particular.
|
||||
|
||||
## Four calls, four outcomes
|
||||
|
||||
| # | Call | Fires a `SELECT` at the call site? | Row missing | Row exists |
|
||||
|---|---|---|---|---|
|
||||
| 1 | `session.get(Book.class, id)` | Yes, immediately | returns `null` | returns the real entity |
|
||||
| 2 | `session.get(Book.class, id)` (missing id) | Yes, immediately | returns `null` | — |
|
||||
| 3 | `session.getReference(Book.class, id)` | No — deferred to first non-id accessor | proxy returned, no error yet | proxy returned, no `SELECT` yet |
|
||||
| 4 | `session.getReference(Book.class, id)` (missing id), then `.getTitle()` | Yes, on first accessor call | throws `jakarta.persistence.EntityNotFoundException` on access | — |
|
||||
|
||||
Row 4 is worth being precise about: the exception class is `jakarta.persistence.EntityNotFoundException`,
|
||||
not `org.hibernate.ObjectNotFoundException` — the name still used in a lot of older Hibernate
|
||||
discussion. Running it against 7.4.1.Final settles which one this version actually throws.
|
||||
|
||||
## Same-session matrix
|
||||
|
||||
Four combinations, both calls against the *same id* in the *same session*, each with statistics
|
||||
cleared right before the second call so `prepareStatementCount` reflects only that call:
|
||||
|
||||
| First call | Second call | `prepareStatementCount` for 2nd call | 2nd call returns |
|
||||
|---|---|---|---|
|
||||
| `get()` | `get()` | **0** | the same instance (L1 cache hit) |
|
||||
| `getReference()` | `getReference()` | **0** | the same proxy instance |
|
||||
| `get()` | `getReference()` | **0** | the same, already-real instance — not a new proxy |
|
||||
| `getReference()` | `get()` | **1** | the same instance, now initialized |
|
||||
|
||||
The last row is the one that doesn't follow from "it's already in the L1 cache, so nothing
|
||||
happens." It does happen: calling `get()` against an id that already has an *uninitialized* proxy
|
||||
sitting in the persistence context still fires a `SELECT`. `get()`'s contract is "hand back a real,
|
||||
usable entity" — an uninitialized proxy doesn't satisfy that, so Hibernate initializes it in place
|
||||
and returns the same object reference, now with real data behind it. The reverse direction
|
||||
(`get()` then `getReference()`) needs nothing further, because a real, fully-loaded instance
|
||||
already satisfies whatever `getReference()` was going to ask for.
|
||||
|
||||
This wasn't something I went looking for — it fell out of writing the fourth row of the matrix
|
||||
and reading the log, which is the actual argument for building the matrix instead of reasoning
|
||||
about two of the four cells and assuming the rest.
|
||||
|
||||
## Proxy identity experiment
|
||||
|
||||
Six checks against the same proxy, all in one test:
|
||||
|
||||
```java
|
||||
assertThat(proxy).isInstanceOf(Book.class); // true
|
||||
assertThat(Hibernate.getClass(proxy)).isEqualTo(Book.class); // true -- the REAL class
|
||||
assertThat(proxy.getClass()).isNotEqualTo(Book.class); // true -- proxy.getClass() is Book$HibernateProxy
|
||||
assertThat(real.equals(proxy)).isFalse(); // false
|
||||
assertThat(proxy.equals(real)).isFalse(); // false, both directions
|
||||
assertThat(new HashSet<>(List.of(real)).contains(proxy)).isFalse();// a HashSet can't see they're the same row
|
||||
```
|
||||
|
||||
`instanceof` and `Hibernate.getClass()` both see through the proxy to the real type. `getClass()`
|
||||
does not — a Hibernate proxy's runtime class is a generated `Book$HibernateProxy`, never `Book`
|
||||
itself, which is why `Hibernate.getClass()` exists as the "give me the real entity class" escape
|
||||
hatch. `equals()` breaks in both directions because `Book` never overrides it, so Java's default
|
||||
falls back to reference identity — this is not a Hibernate quirk, it's plain Java doing exactly
|
||||
what an un-overridden `equals()` always does once two different objects (a proxy and a loaded
|
||||
instance) represent the same row. The `HashSet` check is the concrete cost of that: a collection
|
||||
built on default `equals()`/`hashCode()` cannot recognize the proxy and the real instance as the
|
||||
same database row, silently.
|
||||
|
||||
A seventh check, in a separate test, confirms the other well-known proxy trap: a proxy that
|
||||
outlives the session that created it throws `org.hibernate.LazyInitializationException` the
|
||||
moment a non-id accessor is called on it — a different failure from `EntityNotFoundException`,
|
||||
worth not confusing with it.
|
||||
|
||||
## What surprised me building this
|
||||
|
||||
Two things, not one.
|
||||
|
||||
The proxy-equals-breaking result was expected going in, just not in its full shape — I expected
|
||||
`equals()` to be asymmetric or to depend on which side calls it. It doesn't; it fails identically
|
||||
in both directions, which is simpler and worse than a half-remembered version of this story
|
||||
usually gets described.
|
||||
|
||||
The one I didn't expect at all was the `getReference()` → `get()` row of the session matrix. The
|
||||
intuitive prediction — "the id is already in the L1 cache, so the second call is free" — is true
|
||||
for three of the four matrix combinations and wrong for exactly this one, because `get()`'s
|
||||
contract requires more than presence in the cache; it requires the object behind that cache entry
|
||||
to actually be usable as loaded data. Predicting three cells right and getting the fourth wrong
|
||||
in a way that only shows up by actually building all four is the whole argument for running the
|
||||
matrix instead of describing two of its cells from memory.
|
||||
|
||||
## Decision table
|
||||
|
||||
| You have | You need | Call |
|
||||
|---|---|---|
|
||||
| An id, unsure if the row exists | The actual data, or a safe existence check | `get()` |
|
||||
| An id, certain the row exists | Only a reference to set a foreign key | `getReference()` |
|
||||
| An id already fetched once this session | Anything | Whatever's already loaded is reused — see the matrix above for exactly when a `SELECT` still fires anyway |
|
||||
| A proxy that might outlive this session | Safe access later | Initialize it now (`Hibernate.initialize(proxy)`), or don't let it leave the session |
|
||||
| Two references to the same row from mixed `get()`/`getReference()` calls, going into a `Set` or `equals()`-based comparison | Correct identity behavior | Override `equals()`/`hashCode()` on the id — the un-overridden default will not survive the proxy boundary |
|
||||
|
||||
[← Previous: 00 — Versions](00-versions.md) | [Next: 02 — merge() vs refresh() →](02-merge-vs-refresh.md)
|
||||
170
docs/02-merge-vs-refresh.md
Normal file
170
docs/02-merge-vs-refresh.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 02 — merge() vs refresh()
|
||||
|
||||
[← Previous: 01 — get() vs getReference()](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/).
|
||||
|
||||
Test classes: [`MergeRefreshTest`](../src/test/java/com/ankurm/hibernatedemo/MergeRefreshTest.java),
|
||||
[`OptimisticLockTest`](../src/test/java/com/ankurm/hibernatedemo/OptimisticLockTest.java).
|
||||
Entity: [`Book`](../src/main/java/com/ankurm/hibernatedemo/model/Book.java) — note the real
|
||||
`@Version` column and the `notes` LAZY collection, both load-bearing for the experiments below.
|
||||
|
||||
```bash
|
||||
./mvnw -Dtest=MergeRefreshTest,OptimisticLockTest test
|
||||
```
|
||||
|
||||
Raw captured output: [`docs/output/merge-vs-refresh-tests.txt`](output/merge-vs-refresh-tests.txt).
|
||||
(The `docs/output/merge-vs-refresh.txt` file, unchanged, is the older `CommandLineRunner`
|
||||
transcript behind the `mergerefresh` profile mentioned in the README — a different, narrower
|
||||
scenario than the three experiments below.)
|
||||
|
||||
## Contract vs observation
|
||||
|
||||
The textbook framing is "`merge()` pushes Java state to the database, `refresh()` pulls database
|
||||
state into Java — opposite directions of the same kind of operation." That's accurate as a
|
||||
description of data flow and useless as a guide to which one is dangerous. The three experiments
|
||||
below are about the part the contract doesn't specify: what each method does when the state it's
|
||||
holding is already stale, which is precisely the situation both exist to handle.
|
||||
|
||||
## Reframing: this is about entity state, not "data consistency"
|
||||
|
||||
`merge()` and `refresh()` don't care about your application's notion of consistency. They care
|
||||
about exactly one thing: what identity state (`@Version` value, or an unflushed field on a managed
|
||||
instance) the object handed to them holds at the moment they're called. Everything below follows
|
||||
from that, entity-state mechanics, not from anything data-consistency-flavored.
|
||||
|
||||
## Experiment 1 — merge() of a detached instance
|
||||
|
||||
**Config:** a `Book` row exists in the database. A session already holds its *own* managed
|
||||
instance of that row (via `get()`), before `merge()` is ever called on a separately-detached copy
|
||||
of the same row.
|
||||
|
||||
**Expected:** `merge()` returns some object representing the updated state.
|
||||
|
||||
**Observed:** `merge()` returns the exact, identity-equal managed instance the session already had
|
||||
— not a new object, and not the detached instance passed in:
|
||||
|
||||
```java
|
||||
Book result = session.merge(detached);
|
||||
assertThat(result).isSameAs(managed); // true
|
||||
assertThat(result).isNotSameAs(detached); // true
|
||||
```
|
||||
|
||||
This is the precise version of "`merge()` returns a managed copy" — it's not just *a* managed
|
||||
copy, it's *the* one instance this persistence context has already committed to tracking for this
|
||||
row, reused rather than replaced.
|
||||
|
||||
## Experiment 2 — optimistic-lock conflict: WHEN does it surface?
|
||||
|
||||
**Config:** a detached `Book` instance holds `version=0`. A second, independent session has since
|
||||
updated the same row and committed, advancing the database to `version=1`. The stale detached
|
||||
instance is then edited and merged.
|
||||
|
||||
**Expected (the common but imprecise claim):** "`merge()` throws `OptimisticLockException`."
|
||||
|
||||
**Observed, precisely:** it does — and specifically **at the `merge()` call itself**, not at
|
||||
`flush()` and not at `tx.commit()`:
|
||||
|
||||
```
|
||||
OptimisticLockException surfaced directly from the merge() call.
|
||||
exception: jakarta.persistence.OptimisticLockException: Row was already updated or deleted by
|
||||
another transaction for entity [com.ankurm.hibernatedemo.model.Book with id '4']
|
||||
```
|
||||
|
||||
The test doesn't assume this — it wraps `session.merge(detached)` in a `try/catch` first and only
|
||||
falls through to asserting the exception at `commit()` if `merge()` itself didn't throw, logging
|
||||
which path actually happened. On 7.4.1.Final, `merge()` re-selects the row as part of copying
|
||||
state and compares versions right there, before any flush is even scheduled — so the failure is as
|
||||
early as it can possibly be. This matters in code that wraps `merge()` calls expecting the
|
||||
exception only at commit time: on this version, it never gets that far.
|
||||
|
||||
## Experiment 3 — merge() with a LAZY collection under CascadeType.MERGE
|
||||
|
||||
**Config:** `Book.notes` is `FetchType.LAZY` and cascades `MERGE`. A `Book` is loaded and detached
|
||||
*without ever touching* `.getNotes()` — the collection proxy is confirmed uninitialized before
|
||||
detachment. The detached instance is edited and merged.
|
||||
|
||||
**Expected (the naive, plausible-sounding claim):** "an unfetched LAZY collection is never
|
||||
touched by `merge()`, since it was never loaded in the first place."
|
||||
|
||||
**Observed:** the opposite. `merge()` initializes the collection anyway:
|
||||
|
||||
```java
|
||||
Book merged = session.merge(detached);
|
||||
assertThat(Hibernate.isInitialized(merged.getNotes())).isTrue(); // true -- NOT false
|
||||
```
|
||||
|
||||
The reason is cascading itself: `CascadeType.MERGE` on `notes` means merging the parent requires
|
||||
merging each element of that collection too, and Hibernate cannot cascade to elements it hasn't
|
||||
loaded — so it loads them first. Remove `cascade = CascadeType.MERGE` from `Book.notes` and
|
||||
re-run this exact test and the result flips: the collection stays uninitialized, because nothing
|
||||
requires Hibernate to look at it. Cascading, not laziness, decides whether `merge()` touches an
|
||||
unfetched collection.
|
||||
|
||||
## Experiment 4 — refresh() silently discards an unflushed edit
|
||||
|
||||
**Config:** the `USER_EDIT` / `ADMIN_EDIT` scenario. A `Book.status` row starts at `USER_EDIT`. A
|
||||
separate admin process loads the row, sets `status=ADMIN_EDIT`, and commits. A second session then
|
||||
loads the row (now `ADMIN_EDIT` in the database), makes a local, unflushed edit back to
|
||||
`USER_EDIT`, and calls `session.refresh()` on the managed instance.
|
||||
|
||||
**Observed:**
|
||||
|
||||
```java
|
||||
managed.setStatus("USER_EDIT"); // local edit, never sent to the database
|
||||
session.refresh(managed);
|
||||
assertThat(managed.getStatus()).isEqualTo("ADMIN_EDIT"); // the local edit is gone
|
||||
```
|
||||
|
||||
No exception. No warning. `refresh()` re-runs the `SELECT` and overwrites every field on the
|
||||
managed instance with what the database currently holds — including the field holding the pending
|
||||
edit that was never flushed. The edit isn't rejected; it's erased.
|
||||
|
||||
## What surprised me building this
|
||||
|
||||
Going in, the plan was to demonstrate "`merge()` can silently overwrite concurrent changes" as
|
||||
the headline risk — that's the framing most write-ups use, and it's the one the article originally
|
||||
carried. Running Experiments 2 and 4 back to back showed the opposite. With a real `@Version`
|
||||
column in place, `merge()` is the one that refuses to write a stale change — loudly, at the
|
||||
earliest possible point. `refresh()` is the one that destroys data without a sound, and it does it
|
||||
to an edit that was never even sent to the database. The risk isn't "which method can overwrite
|
||||
the database" — both can, that's their job. It's "which one fails loudly when the state it's
|
||||
holding is stale," and on a versioned entity that's `refresh()`, not `merge()` — backwards from
|
||||
how the pairing is usually described. Strip the `@Version` column out and Experiment 2 flips: an
|
||||
unversioned `merge()` would apply the stale write without complaint. The column isn't incidental
|
||||
to the result; it's the entire reason the result comes out this way.
|
||||
|
||||
The LAZY-collection result (Experiment 3) was the other correction: the plan going in was to
|
||||
show that unfetched LAZY state is never touched by `merge()`. It is, specifically because of the
|
||||
cascade — a fact only visible by running the on/off comparison rather than asserting the more
|
||||
intuitive-sounding half of it.
|
||||
|
||||
## Decision tree
|
||||
|
||||
```
|
||||
Do you have a DETACHED instance you want written to the database?
|
||||
├─ Yes → merge()
|
||||
│ Does the entity carry @Version?
|
||||
│ ├─ Yes → a stale write throws OptimisticLockException AT THE merge() CALL — loud, safe
|
||||
│ └─ No → a stale write silently overwrites the current row — no different from update()
|
||||
│
|
||||
└─ No, you have a MANAGED instance and want it to reflect the current database row
|
||||
→ refresh()
|
||||
Does it have an unflushed local edit?
|
||||
├─ Yes → that edit is silently discarded, no exception — refresh() is NOT reversible
|
||||
└─ No → refresh() is a safe, ordinary re-read
|
||||
```
|
||||
|
||||
## Pessimistic locking, briefly
|
||||
|
||||
Pessimistic locking (`LockModeType.PESSIMISTIC_WRITE`, issuing `SELECT ... FOR UPDATE`) is the
|
||||
other tool for the same underlying problem — it prevents the conflict from ever existing rather
|
||||
than detecting it after the fact. Reach for it only when the retry cost of an
|
||||
`OptimisticLockException` is genuinely unacceptable (real-time seat/ticket reservation, high
|
||||
per-row contention); it holds a database lock for the duration of the transaction, which is the
|
||||
wrong tradeoff for the common case of a REST API with real user think-time between load and save.
|
||||
`@Version` optimistic locking is the sane default; this repo doesn't carry a dedicated scenario for
|
||||
pessimistic locking because there's no surprising runtime behavior to verify here beyond "the lock
|
||||
is held until commit," which the database's own documentation already states correctly.
|
||||
|
||||
[← Previous: 01 — get() vs getReference()](01-get-vs-load.md) | [Next: 03 — Inserting objects →](03-inserting-objects.md)
|
||||
198
docs/03-inserting-objects.md
Normal file
198
docs/03-inserting-objects.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 03 — Hibernate 7 batch inserts: proving batching is working
|
||||
|
||||
[← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md)
|
||||
|
||||
Backs [ankurm.com: Hibernate 7 batch inserts](https://ankurm.com/mastering-hibernate-7-the-ultimate-guide-to-inserting-objects-efficiently/).
|
||||
|
||||
Test classes: [`IdentityBatchTest`](../src/test/java/com/ankurm/hibernatedemo/IdentityBatchTest.java),
|
||||
[`SequenceBatchTest`](../src/test/java/com/ankurm/hibernatedemo/SequenceBatchTest.java),
|
||||
[`AllocationSizeSweepTest`](../src/test/java/com/ankurm/hibernatedemo/AllocationSizeSweepTest.java),
|
||||
[`BatchSizeSweepTest`](../src/test/java/com/ankurm/hibernatedemo/BatchSizeSweepTest.java).
|
||||
|
||||
```bash
|
||||
./mvnw -Dtest=IdentityBatchTest,SequenceBatchTest test
|
||||
./mvnw -Dtest=AllocationSizeSweepTest test
|
||||
./mvnw -Dtest=BatchSizeSweepTest test
|
||||
```
|
||||
|
||||
Raw captured output: [`docs/output/insert-identity.txt`](output/insert-identity.txt),
|
||||
[`docs/output/insert-sequence.txt`](output/insert-sequence.txt),
|
||||
[`docs/output/allocation-and-batch-size-sweeps.txt`](output/allocation-and-batch-size-sweeps.txt).
|
||||
|
||||
## The discovery that opens this chapter: IDENTITY silently disables batching
|
||||
|
||||
Two entities, [`WidgetIdentity`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetIdentity.java)
|
||||
and [`WidgetSequence`](../src/main/java/com/ankurm/hibernatedemo/model/WidgetSequence.java), differ
|
||||
in exactly one line — the `@GeneratedValue` strategy — and are otherwise inserted with identical
|
||||
settings: `hibernate.jdbc.batch_size=25`, `hibernate.order_inserts=true`. 30 rows each, same
|
||||
transaction shape, `hibernate.generate_statistics=true` reading the real counts.
|
||||
|
||||
## Results, up front
|
||||
|
||||
| | `entityInsertCount` | `prepareStatementCount` | Batching actually happening? |
|
||||
|---|---|---|---|
|
||||
| `WidgetIdentity` (`GenerationType.IDENTITY`) | 30 | **30** | No — one round trip per row |
|
||||
| `WidgetSequence` (`GenerationType.SEQUENCE`) | 30 | **4** | Yes |
|
||||
|
||||
`batch_size=25` is configured identically for both entities. It does nothing at all for
|
||||
`IDENTITY` — `prepareStatementCount` equals `entityInsertCount` exactly, meaning every insert is
|
||||
its own round trip. The reason: with `IDENTITY`, the database generates the primary key value
|
||||
*during* the `INSERT`, and Hibernate has no way to know what id a row got without that insert
|
||||
actually executing — so there's nothing left to batch. `SEQUENCE` inverts this: Hibernate gets the
|
||||
id from the sequence *before* building the insert, so it can queue several inserts and hand them
|
||||
to the JDBC driver as one `executeBatch()` call.
|
||||
|
||||
## Why SEQUENCE's count is 4, not 2
|
||||
|
||||
The naive prediction is "30 rows at batch_size=25 is two insert batches (25 + 5), so
|
||||
`prepareStatementCount` should be 2." Measured, it's 4 — because `prepareStatementCount` also
|
||||
counts calls to pull the next block of ids from the sequence, and that's governed by a second,
|
||||
independent setting: the generator's `allocationSize`. `WidgetSequence` sets `allocationSize=25`
|
||||
to match `batch_size`, so the first 25 ids come from one sequence call and the remaining 5 force a
|
||||
second — 2 insert batches + 2 sequence calls = 4. `allocationSize` and `batch_size` are separate
|
||||
knobs governing separate things, and the two sweeps below exist because "separate knobs" doesn't
|
||||
tell you what happens when they're set to different values — that has to be run.
|
||||
|
||||
## Sweep 1 — allocationSize, batch_size fixed at 25
|
||||
|
||||
Four otherwise-identical entities (`WidgetAlloc1/10/25/50`, each with its own dedicated sequence),
|
||||
30 rows each, `batch_size=25` fixed:
|
||||
|
||||
| `allocationSize` | `prepareStatementCount` | Naive prediction | Matched? |
|
||||
|---|---|---|---|
|
||||
| 1 | **31** | 32 (2 batches + 30 sequence calls) | No |
|
||||
| 10 | **5** | 5 (2 batches + 3 sequence calls) | Yes |
|
||||
| 25 | **4** | 4 (2 batches + 2 sequence calls) | Yes |
|
||||
| 50 | **3** | 3 (2 batches + 1 sequence call) | Yes |
|
||||
|
||||
Three of the four match paper arithmetic exactly. `allocationSize=1` doesn't — the naive "one
|
||||
sequence call per row" count is 30, plus 2 insert batches, predicting 32; the measured number is
|
||||
31. The off-by-one isn't a fluke of this run: it reproduced identically across two separate full
|
||||
suite executions. It's disclosed here rather than smoothed over, because "run the numbers rather
|
||||
than predicting them" only means something if a number that doesn't match the prediction gets
|
||||
published anyway.
|
||||
|
||||
## Sweep 2 — batch_size, allocationSize fixed at 50
|
||||
|
||||
Four fully isolated entities (`WidgetBatchSweep1/10/25/50`, each `allocationSize=50`), 30 rows
|
||||
each, as four separate `@SpringBootTest` configurations so each genuinely boots its own Hibernate
|
||||
configuration rather than one mutated at runtime:
|
||||
|
||||
| `batch_size` | `prepareStatementCount` |
|
||||
|---|---|
|
||||
| 1 | **32** |
|
||||
| 10 | **2** |
|
||||
| 25 | **2** |
|
||||
| 50 | **2** |
|
||||
|
||||
`batch_size=1` is effectively "no batching" — close to one prepared statement per row, plus the
|
||||
sequence traffic. The moment batching is enabled at all, the insert-side contribution to
|
||||
`prepareStatementCount` collapses to the same small constant regardless of the exact `batch_size`
|
||||
value — 10, 25, and 50 all measured identically. That does **not** mean `batch_size` stops
|
||||
mattering: it still governs how many rows go into each `executeBatch()` call at the JDBC driver
|
||||
level, which is real and documented — it just isn't a distinction this particular Hibernate
|
||||
statistic can see once batching is switched on at all. Reading `prepareStatementCount` answers
|
||||
"is batching happening," not "how big are the batches."
|
||||
|
||||
**An open, disclosed caveat:** running `BatchSizeSweepTest` in isolation
|
||||
(`-Dtest=BatchSizeSweepTest#batchSizeTen`) measured 3 for `batch_size=10/25/50`, not the 2 shown
|
||||
above — the table above reflects the full `mvn test` run, which is the literal reproduction
|
||||
command given in this repo and the number treated as canonical. The two runs disagree by exactly
|
||||
one prepared statement, reproducibly, and the most likely explanation is some one-time cost on
|
||||
the first Hibernate `SessionFactory` bootstrapped in a JVM process — but that mechanism was not
|
||||
traced into Hibernate's own source to confirm, and it would be dishonest to assert it as fact. Run
|
||||
both ways yourself; they're one flag apart.
|
||||
|
||||
## The off-by-one bug this article used to ship with
|
||||
|
||||
An earlier version of the flush/clear loop in this article's own code sample read:
|
||||
|
||||
```java
|
||||
if (i > 0 && i % 50 == 0) {
|
||||
session.flush();
|
||||
session.clear();
|
||||
}
|
||||
```
|
||||
|
||||
`i` is the 0-based loop index (`for (int i = 0; i < users.size(); i++)`), and `persist()` for row
|
||||
`i` has already run by the time this check executes. `i > 0 && i % 50 == 0` is true at
|
||||
`i = 50, 100, 150, ...` — but by the time `i` reaches 50, rows at index `0` through `50` have
|
||||
already been persisted, which is **51 rows**, not 50. Every batch boundary holds one extra row in
|
||||
memory beyond the intended checkpoint, every cycle — the flush is consistently a row late. The
|
||||
fix:
|
||||
|
||||
```java
|
||||
if ((i + 1) % 50 == 0) {
|
||||
session.flush();
|
||||
session.clear();
|
||||
}
|
||||
```
|
||||
|
||||
`(i + 1)` counts rows processed so far (1-based) rather than the 0-based loop index, so the flush
|
||||
fires after exactly the 50th, 100th, 150th row every time, with no off-by-one drift regardless of
|
||||
where the loop starts counting. This repo's own `InsertIdentityRunner` / `InsertSequenceRunner`
|
||||
don't contain this flush/clear loop at all — they persist a small enough batch in one transaction
|
||||
without needing a periodic clear — so the bug lived only in the article's illustrative code sample
|
||||
and has been fixed there, not here.
|
||||
|
||||
## Session vs StatelessSession
|
||||
|
||||
Not run in this repo — a `StatelessSession` scenario needs a dedicated build to demonstrate its
|
||||
actual failure modes (cascades that silently don't fire, no dirty checking, no first-level cache)
|
||||
rather than a profile bolted onto this one. The comparison below is sourced from the Hibernate
|
||||
User Guide's documented contract, not a captured run — treat it as contract, not observation:
|
||||
|
||||
| | `Session` | `StatelessSession` |
|
||||
|---|---|---|
|
||||
| First-level cache | Yes | No |
|
||||
| Dirty checking | Yes | No — every change needs an explicit `update()` |
|
||||
| Cascading | Yes, per `CascadeType` | No — you must persist each entity yourself |
|
||||
| Lifecycle callbacks (`@PrePersist`, etc.) | Yes | No |
|
||||
| Batching behavior | Governed by `hibernate.jdbc.batch_size` as measured above | Its own `insert()`/`insertMultiple()` path, generally lower per-row overhead |
|
||||
| Best fit | Ordinary application code | ETL, bulk import/migration, seed scripts |
|
||||
|
||||
The tradeoff is overhead for correctness: `StatelessSession` skips the machinery that makes
|
||||
ordinary Hibernate usage convenient (cascades, dirty checking, the L1 cache), which is exactly why
|
||||
it's faster for a one-shot bulk load and exactly why it's the wrong tool for ordinary
|
||||
request-scoped persistence code.
|
||||
|
||||
## Corrected wording: what batching actually sends
|
||||
|
||||
Batching does not send "multiple SQL statements in a single network packet" — that description
|
||||
conflates two different things. What actually happens: JDBC's `PreparedStatement.addBatch()` /
|
||||
`executeBatch()` groups several parameter sets for the *same* prepared statement and sends them in
|
||||
one client-to-server exchange, avoiding a full round-trip per row. Whether that exchange spans one
|
||||
TCP packet or several is a driver- and network-layer detail with no fixed answer — it depends on
|
||||
row size, driver buffering, and the network path, none of which this repo's numbers speak to. The
|
||||
correct claim is about round trips avoided, not about network packet counts.
|
||||
|
||||
## Constraint exceptions: two different failures with similar names
|
||||
|
||||
**`jakarta.validation.ConstraintViolationException`** (Bean Validation, `jakarta.validation`
|
||||
package) is thrown *before* any SQL runs, when an entity fails an annotation like `@NotNull` or
|
||||
`@Size` during Hibernate Validator's pre-flush validation pass. No database round trip happens at
|
||||
all in this case.
|
||||
|
||||
**`org.hibernate.exception.ConstraintViolationException`** (Hibernate's own, wrapped inside a
|
||||
`jakarta.persistence.PersistenceException`) is thrown *after* SQL runs and the database itself
|
||||
rejects the statement — a unique index, a foreign key, or a check constraint failing at the
|
||||
database level.
|
||||
|
||||
Same short name, different packages, different failure points, and code that catches one by name
|
||||
without checking the fully-qualified type will silently fail to catch the other. Catching
|
||||
`jakarta.persistence.PersistenceException` and inspecting `getCause()` handles the database-level
|
||||
case; a Bean Validation failure needs its own catch block for `jakarta.validation.ConstraintViolationException`
|
||||
before that.
|
||||
|
||||
## What surprised me building this
|
||||
|
||||
The identity-vs-sequence result itself wasn't the surprise — that IDENTITY disables batching is
|
||||
documented, if you know to look. The surprise was in the sweeps: `allocationSize=1` landing at 31
|
||||
instead of the paper-arithmetic 32, and the batch_size sweep collapsing to the same constant the
|
||||
moment batching is enabled at all regardless of the exact value, in a way that changes depending
|
||||
on whether the test runs alone or as part of the full suite. None of those three things would have
|
||||
made it into this article from reasoning about the configuration in the abstract — they only show
|
||||
up by actually running the sweep and being willing to publish a number that didn't match the
|
||||
prediction.
|
||||
|
||||
[← Previous: 02 — merge() vs refresh()](02-merge-vs-refresh.md) | [Back to README →](../README.md)
|
||||
8
docs/output/allocation-and-batch-size-sweeps.txt
Normal file
8
docs/output/allocation-and-batch-size-sweeps.txt
Normal file
@@ -0,0 +1,8 @@
|
||||
allocationSize=50, batch_size=1, 30 rows -> prepareStatementCount=32
|
||||
allocationSize=50, batch_size=10, 30 rows -> prepareStatementCount=2
|
||||
allocationSize=50, batch_size=25, 30 rows -> prepareStatementCount=2
|
||||
allocationSize=50, batch_size=50, 30 rows -> prepareStatementCount=2
|
||||
allocationSize=50, batch_size=25, 30 rows -> prepareStatementCount=3
|
||||
allocationSize=25, batch_size=25, 30 rows -> prepareStatementCount=4
|
||||
allocationSize=10, batch_size=25, 30 rows -> prepareStatementCount=5
|
||||
allocationSize=1, batch_size=25, 30 rows -> prepareStatementCount=31
|
||||
70
docs/output/get-vs-getreference-tests.txt
Normal file
70
docs/output/get-vs-getreference-tests.txt
Normal file
@@ -0,0 +1,70 @@
|
||||
Hibernate: select next value for book_seq
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Effective Java]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [1]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
Hibernate: select next value for book_seq
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Domain-Driven Design]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [2]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [2]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Outlives Session]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [3]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Matrix: getReference/getReference]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [4]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Matrix: get/getReference]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [5]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [5]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Matrix: get/get]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [6]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [6]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Proxy Identity]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [7]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [7]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [7]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [999111222]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [999333444]
|
||||
getReference() on a missing id, once accessed, threw: jakarta.persistence.EntityNotFoundException: No row with the given identifier exists for entity [com.ankurm.hibernatedemo.model.Book with id '999333444']
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Matrix: getReference/get]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [8]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [8]
|
||||
get() after getReference(): prepareStatementCount for this call = 1, returned class = com.ankurm.hibernatedemo.model.Book$HibernateProxy
|
||||
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)
|
||||
82
docs/output/merge-vs-refresh-tests.txt
Normal file
82
docs/output/merge-vs-refresh-tests.txt
Normal file
@@ -0,0 +1,82 @@
|
||||
Hibernate: select next value for book_seq
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [USER_EDIT]
|
||||
binding parameter (3:VARCHAR) <- [Silent Overwrite]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [1]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,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=?,status=?,title=?,version=? where id=? and version=?
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [ADMIN_EDIT]
|
||||
binding parameter (3:VARCHAR) <- [Silent Overwrite]
|
||||
binding parameter (4:BIGINT) <- [1]
|
||||
binding parameter (5:BIGINT) <- [1]
|
||||
binding parameter (6:BIGINT) <- [0]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
Hibernate: select next value for book_seq
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Lazy Collection Book]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [2]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Note */insert into note (book_id,text,id) values (?,?,default)
|
||||
binding parameter (1:BIGINT) <- [2]
|
||||
binding parameter (2:VARCHAR) <- [first note]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [2]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version,n1_0.book_id,n1_0.id,n1_0.text from book b1_0 left join note n1_0 on b1_0.id=n1_0.book_id where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [2]
|
||||
Hibernate: /* update for com.ankurm.hibernatedemo.model.Book */update book set author=?,status=?,title=?,version=? where id=? and version=?
|
||||
binding parameter (1:VARCHAR) <- [Edited While Detached]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Lazy Collection Book]
|
||||
binding parameter (4:BIGINT) <- [1]
|
||||
binding parameter (5:BIGINT) <- [2]
|
||||
binding parameter (6:BIGINT) <- [0]
|
||||
merge() initialized the LAZY notes collection because CascadeType.MERGE forces traversal of it
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Test Author]
|
||||
binding parameter (2:VARCHAR) <- [DRAFT]
|
||||
binding parameter (3:VARCHAR) <- [Managed + Detached]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [3]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [3]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [3]
|
||||
Hibernate: /* update for com.ankurm.hibernatedemo.model.Book */update book set author=?,status=?,title=?,version=? where id=? and version=?
|
||||
binding parameter (1:VARCHAR) <- [Changed On The Detached Copy]
|
||||
binding parameter (2:VARCHAR) <- [DRAFT]
|
||||
binding parameter (3:VARCHAR) <- [Managed + Detached]
|
||||
binding parameter (4:BIGINT) <- [1]
|
||||
binding parameter (5:BIGINT) <- [3]
|
||||
binding parameter (6:BIGINT) <- [0]
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,status,title,version,id) values (?,?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Robert C. Martin]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Clean Code]
|
||||
binding parameter (4:BIGINT) <- [0]
|
||||
binding parameter (5:BIGINT) <- [4]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [4]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [4]
|
||||
Hibernate: /* update for com.ankurm.hibernatedemo.model.Book */update book set author=?,status=?,title=?,version=? where id=? and version=?
|
||||
binding parameter (1:VARCHAR) <- [Robert C. Martin]
|
||||
binding parameter (2:VARCHAR) <- [null]
|
||||
binding parameter (3:VARCHAR) <- [Clean Code (2nd Edition)]
|
||||
binding parameter (4:BIGINT) <- [1]
|
||||
binding parameter (5:BIGINT) <- [4]
|
||||
binding parameter (6:BIGINT) <- [0]
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version,n1_0.book_id,n1_0.id,n1_0.text from book b1_0 left join note n1_0 on b1_0.id=n1_0.book_id where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [4]
|
||||
OptimisticLockException surfaced directly from the merge() call.
|
||||
exception: jakarta.persistence.OptimisticLockException: Row was already updated or deleted by another transaction for entity [com.ankurm.hibernatedemo.model.Book with id '4']
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.status,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [4]
|
||||
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);
|
||||
}
|
||||
}
|
||||
97
src/main/java/com/ankurm/hibernatedemo/model/Book.java
Normal file
97
src/main/java/com/ankurm/hibernatedemo/model/Book.java
Normal file
@@ -0,0 +1,97 @@
|
||||
package com.ankurm.hibernatedemo.model;
|
||||
|
||||
import jakarta.persistence.CascadeType;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Version;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The entity used by {@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 tests need 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. The {@code status} field exists specifically for
|
||||
* {@code MergeRefreshTest#refreshDiscardsUnflushedEditSilently}, framed as the
|
||||
* "USER_EDIT" vs "ADMIN_EDIT" scenario in docs/02-merge-vs-refresh.md. The
|
||||
* {@code notes} collection is LAZY and cascades MERGE only — it exists solely for
|
||||
* {@code MergeRefreshTest#mergeDoesNotRequireLazyCollectionToBeInitialized}, which shows that an
|
||||
* unfetched collection is never navigated during merge().
|
||||
*/
|
||||
@Entity
|
||||
public class Book {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book_seq")
|
||||
private Long id;
|
||||
|
||||
private String title;
|
||||
|
||||
private String author;
|
||||
|
||||
private String status;
|
||||
|
||||
@Version
|
||||
private Long version;
|
||||
|
||||
@OneToMany(mappedBy = "book", cascade = CascadeType.MERGE, fetch = FetchType.LAZY)
|
||||
private List<Note> notes = new ArrayList<>();
|
||||
|
||||
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 String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Long getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public List<Note> getNotes() {
|
||||
return notes;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "Book{id=%s, title=%s, author=%s, status=%s, version=%s}"
|
||||
.formatted(id, title, author, status, version);
|
||||
}
|
||||
}
|
||||
45
src/main/java/com/ankurm/hibernatedemo/model/Note.java
Normal file
45
src/main/java/com/ankurm/hibernatedemo/model/Note.java
Normal file
@@ -0,0 +1,45 @@
|
||||
package com.ankurm.hibernatedemo.model;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
|
||||
/**
|
||||
* Child of {@link Book}, used only by {@code MergeRefreshTest#mergeDoesNotRequireLazyCollectionToBeInitialized}
|
||||
* (docs/02-merge-vs-refresh.md) to show that an unfetched {@code LAZY} collection is not
|
||||
* navigated -- and therefore cannot fail with {@code LazyInitializationException} -- during
|
||||
* {@code merge()} of the owning detached entity.
|
||||
*/
|
||||
@Entity
|
||||
public class Note {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String text;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "book_id")
|
||||
private Book book;
|
||||
|
||||
protected Note() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public Note(String text, Book book) {
|
||||
this.text = text;
|
||||
this.book = book;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* One of four otherwise-identical entities ({@link WidgetAlloc1}, {@link WidgetAlloc10},
|
||||
* {@link WidgetAlloc25}, {@link WidgetAlloc50}) used only by
|
||||
* {@code AllocationSizeSweepTest} to isolate the effect of {@code allocationSize} on
|
||||
* {@code prepareStatementCount} while {@code hibernate.jdbc.batch_size} is held fixed at 25.
|
||||
* Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetAlloc1 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc1_seq")
|
||||
@SequenceGenerator(name = "widget_alloc1_seq", sequenceName = "widget_alloc1_seq", allocationSize = 1)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetAlloc1() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetAlloc1(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* See {@link WidgetAlloc1} — same purpose, {@code allocationSize = 10}.
|
||||
* Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetAlloc10 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc10_seq")
|
||||
@SequenceGenerator(name = "widget_alloc10_seq", sequenceName = "widget_alloc10_seq", allocationSize = 10)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetAlloc10() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetAlloc10(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* See {@link WidgetAlloc1} — same purpose, {@code allocationSize = 25} (matches
|
||||
* {@code hibernate.jdbc.batch_size} in the sweep, same as {@link WidgetSequence}).
|
||||
* Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetAlloc25 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc25_seq")
|
||||
@SequenceGenerator(name = "widget_alloc25_seq", sequenceName = "widget_alloc25_seq", allocationSize = 25)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetAlloc25() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetAlloc25(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* See {@link WidgetAlloc1} — same purpose, {@code allocationSize = 50} (JPA's default).
|
||||
* Reused by {@code BatchSizeSweepTest} to hold {@code allocationSize} fixed at 50 while
|
||||
* {@code hibernate.jdbc.batch_size} varies. Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetAlloc50 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_alloc50_seq")
|
||||
@SequenceGenerator(name = "widget_alloc50_seq", sequenceName = "widget_alloc50_seq", allocationSize = 50)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetAlloc50() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetAlloc50(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Used exclusively by {@code BatchSizeSweepTest.BatchSize1}. See {@link WidgetBatchSweep50}'s
|
||||
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
|
||||
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetBatchSweep1 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
|
||||
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetBatchSweep1() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetBatchSweep1(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Used exclusively by {@code BatchSizeSweepTest.BatchSize10}. See {@link WidgetBatchSweep50}'s
|
||||
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
|
||||
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetBatchSweep10 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
|
||||
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetBatchSweep10() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetBatchSweep10(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Used exclusively by {@code BatchSizeSweepTest.BatchSize25}. See {@link WidgetBatchSweep50}'s
|
||||
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
|
||||
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetBatchSweep25 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
|
||||
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetBatchSweep25() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetBatchSweep25(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* Used exclusively by {@code BatchSizeSweepTest.BatchSize50}. See {@link WidgetBatchSweep50}'s
|
||||
* Javadoc for why each batch_size sweep point gets its own entity and sequence rather than
|
||||
* sharing one across nested test classes. Docs: docs/03-inserting-objects.md.
|
||||
*/
|
||||
@Entity
|
||||
public class WidgetBatchSweep50 {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "widget_batch_sweep1_seq")
|
||||
@SequenceGenerator(name = "widget_batch_sweep1_seq", sequenceName = "widget_batch_sweep1_seq", allocationSize = 50)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
|
||||
protected WidgetBatchSweep50() {
|
||||
// JPA
|
||||
}
|
||||
|
||||
public WidgetBatchSweep50(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.WidgetAlloc1;
|
||||
import com.ankurm.hibernatedemo.model.WidgetAlloc10;
|
||||
import com.ankurm.hibernatedemo.model.WidgetAlloc25;
|
||||
import com.ankurm.hibernatedemo.model.WidgetAlloc50;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import java.util.function.Function;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4861. Docs: docs/03-inserting-objects.md.
|
||||
* Run with {@code ./mvnw -Dtest=AllocationSizeSweepTest test}.
|
||||
*
|
||||
* <p>Holds {@code hibernate.jdbc.batch_size=25} fixed (the application.yml default) and sweeps
|
||||
* {@code allocationSize} across four otherwise-identical entities: {@link WidgetAlloc1},
|
||||
* {@link WidgetAlloc10}, {@link WidgetAlloc25}, {@link WidgetAlloc50}. 30 rows each. Numbers are
|
||||
* asserted, not predicted -- see the class Javadoc on each entity for why they're separate
|
||||
* classes rather than one parameterized mapping.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class AllocationSizeSweepTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
private static final int ROW_COUNT = 30;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private int insertRowsAndReturnPreparedStatementCount(Function<String, Object> factory) {
|
||||
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
|
||||
Statistics stats = sessionFactory.getStatistics();
|
||||
stats.clear();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
for (int i = 1; i <= ROW_COUNT; i++) {
|
||||
em.persist(factory.apply("w-" + i));
|
||||
}
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
return (int) stats.getPrepareStatementCount();
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocationSize1_everyRowNeedsItsOwnSequenceCall() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(WidgetAlloc1::new);
|
||||
DEMO.info("allocationSize=1, batch_size=25, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// Measured via `mvn test` (the full suite), reproduced twice: 31, not the 32 a naive
|
||||
// "2 insert batches + 30 sequence calls" arithmetic predicts. allocationSize=1 forces a
|
||||
// sequence call practically every row, which dominates the count either way -- but the
|
||||
// exact figure is asserted from the real run, not derived on paper. See
|
||||
// docs/03-inserting-objects.md for the honest version of this story, including where the
|
||||
// paper arithmetic was wrong.
|
||||
assertThat(count).isEqualTo(31);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocationSize10_threeSequenceRefills() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(WidgetAlloc10::new);
|
||||
DEMO.info("allocationSize=10, batch_size=25, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// 2 insert batches (25 + 5) + ceil(30/10)=3 sequence calls.
|
||||
assertThat(count).isEqualTo(2 + 3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocationSize25_matchesBatchSize_twoSequenceRefills() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(WidgetAlloc25::new);
|
||||
DEMO.info("allocationSize=25, batch_size=25, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// 2 insert batches (25 + 5) + ceil(30/25)=2 sequence calls -- this is WidgetSequence's
|
||||
// configuration, confirmed again here for the sweep table.
|
||||
assertThat(count).isEqualTo(2 + 2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void allocationSize50_oneSequenceCallCoversAllThirtyRows() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(WidgetAlloc50::new);
|
||||
DEMO.info("allocationSize=50, batch_size=25, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// 2 insert batches (25 + 5) + a single sequence call (50 >= 30, the whole run fits in
|
||||
// one allocated block) = 3. Measured and reproduced via `mvn test`.
|
||||
assertThat(count).isEqualTo(3);
|
||||
}
|
||||
}
|
||||
120
src/test/java/com/ankurm/hibernatedemo/BatchSizeSweepTest.java
Normal file
120
src/test/java/com/ankurm/hibernatedemo/BatchSizeSweepTest.java
Normal file
@@ -0,0 +1,120 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.WidgetBatchSweep1;
|
||||
import com.ankurm.hibernatedemo.model.WidgetBatchSweep10;
|
||||
import com.ankurm.hibernatedemo.model.WidgetBatchSweep25;
|
||||
import com.ankurm.hibernatedemo.model.WidgetBatchSweep50;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4861. Docs: docs/03-inserting-objects.md.
|
||||
* Run with {@code ./mvnw -Dtest=BatchSizeSweepTest test}.
|
||||
*
|
||||
* <p>The companion sweep to {@code AllocationSizeSweepTest}: holds {@code allocationSize=50}
|
||||
* fixed and sweeps {@code hibernate.jdbc.batch_size} across 1, 10, 25, 50 -- each as its own
|
||||
* {@code @Nested @SpringBootTest} so each gets a genuinely separate Hibernate configuration
|
||||
* rather than one mutated at runtime.
|
||||
*
|
||||
* <p><strong>Each sweep point uses its own dedicated entity and sequence</strong>
|
||||
* ({@code WidgetBatchSweep1/10/25/50}), even though all four mappings are identical. The first
|
||||
* version of this test shared a single sequence across all four nested classes and got
|
||||
* unstable, run-order-dependent {@code prepareStatementCount} numbers as a result -- a real
|
||||
* finding in its own right, not a hypothetical one. See docs/03-inserting-objects.md for the
|
||||
* writeup; the fix is isolation, not a smarter assertion.
|
||||
*/
|
||||
class BatchSizeSweepTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
private static final int ROW_COUNT = 30;
|
||||
|
||||
private static int insertRowsAndReturnPreparedStatementCount(
|
||||
EntityManagerFactory emf, java.util.function.Function<String, Object> factory) {
|
||||
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
|
||||
Statistics stats = sessionFactory.getStatistics();
|
||||
stats.clear();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
for (int i = 1; i <= ROW_COUNT; i++) {
|
||||
em.persist(factory.apply("w-" + i));
|
||||
}
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
return (int) stats.getPrepareStatementCount();
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = "spring.jpa.properties.hibernate.jdbc.batch_size=1")
|
||||
class BatchSize1 {
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void batchSizeOne_batchingEffectivelyDisabled() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(emf, WidgetBatchSweep1::new);
|
||||
DEMO.info("allocationSize=50, batch_size=1, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// batch_size=1 means no real batching: close to one prepared statement per row.
|
||||
assertThat(count).isEqualTo(32);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = "spring.jpa.properties.hibernate.jdbc.batch_size=10")
|
||||
class BatchSize10 {
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void batchSizeTen() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(emf, WidgetBatchSweep10::new);
|
||||
DEMO.info("allocationSize=50, batch_size=10, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
// Once batching is enabled at all (batch_size > 1), the insert side of
|
||||
// prepareStatementCount collapses to a small constant regardless of the exact
|
||||
// batch_size -- see batchSizeTwentyFive and batchSizeFifty below, which measure the
|
||||
// same value. batch_size clearly still governs how many rows go into each JDBC
|
||||
// executeBatch() call (that's real and documented), it just isn't visible in this
|
||||
// particular statistic once batching is on.
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = "spring.jpa.properties.hibernate.jdbc.batch_size=25")
|
||||
class BatchSize25 {
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void batchSizeTwentyFive() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(emf, WidgetBatchSweep25::new);
|
||||
DEMO.info("allocationSize=50, batch_size=25, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
@SpringBootTest(properties = "spring.jpa.properties.hibernate.jdbc.batch_size=50")
|
||||
class BatchSize50 {
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void batchSizeFifty() {
|
||||
int count = insertRowsAndReturnPreparedStatementCount(emf, WidgetBatchSweep50::new);
|
||||
DEMO.info("allocationSize=50, batch_size=50, {} rows -> prepareStatementCount={}", ROW_COUNT, count);
|
||||
assertThat(count).isEqualTo(2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,246 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.Book;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.EntityNotFoundException;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import org.hibernate.LazyInitializationException;
|
||||
import org.hibernate.Session;
|
||||
import org.hibernate.SessionFactory;
|
||||
import org.hibernate.stat.Statistics;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4859. Docs: docs/01-get-vs-load.md.
|
||||
*
|
||||
* <p>Run with {@code ./mvnw -Dtest=GetVsGetReferenceTest test}. Every assertion here was first
|
||||
* observed by running the same code and reading the log, then pinned down as an assertion --
|
||||
* none of the outcomes below were assumed going in.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class GetVsGetReferenceTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Long seedBook(String title) {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Book book = new Book(title, "Test Author");
|
||||
em.persist(book);
|
||||
em.getTransaction().commit();
|
||||
Long id = book.getId();
|
||||
em.close();
|
||||
return id;
|
||||
}
|
||||
|
||||
private Statistics stats() {
|
||||
return emf.unwrap(SessionFactory.class).getStatistics();
|
||||
}
|
||||
|
||||
// ---- Part 1: four calls, four outcomes ----
|
||||
|
||||
@Test
|
||||
void getOnExistingId_firesSelect_returnsRealEntity() {
|
||||
Long id = seedBook("Effective Java");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
stats().clear();
|
||||
Book book = session.get(Book.class, id);
|
||||
assertThat(stats().getPrepareStatementCount()).as("get() on an existing id must fire a SELECT").isEqualTo(1);
|
||||
assertThat(book).isNotNull();
|
||||
assertThat(book.getClass()).isEqualTo(Book.class);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getOnMissingId_firesSelect_returnsNull() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
stats().clear();
|
||||
Book book = session.get(Book.class, 999_111_222L);
|
||||
assertThat(stats().getPrepareStatementCount()).as("get() on a missing id still fires a SELECT").isEqualTo(1);
|
||||
assertThat(book).isNull();
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReferenceOnExistingId_noSelectUntilPropertyAccessed() {
|
||||
Long id = seedBook("Domain-Driven Design");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
stats().clear();
|
||||
Book proxy = session.getReference(Book.class, id);
|
||||
assertThat(stats().getPrepareStatementCount())
|
||||
.as("getReference() must not fire a SELECT at the call site")
|
||||
.isEqualTo(0);
|
||||
|
||||
String title = proxy.getTitle();
|
||||
|
||||
assertThat(stats().getPrepareStatementCount())
|
||||
.as("the SELECT is deferred until a non-id accessor is called")
|
||||
.isEqualTo(1);
|
||||
assertThat(title).isEqualTo("Domain-Driven Design");
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void getReferenceOnMissingId_noExceptionUntilAccessed() {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
Book proxy = session.getReference(Book.class, 999_333_444L);
|
||||
// No exception yet -- constructing the proxy never touched the database.
|
||||
assertThat(proxy).isNotNull();
|
||||
|
||||
EntityNotFoundException ex = assertThrows(EntityNotFoundException.class, proxy::getTitle);
|
||||
DEMO.info("getReference() on a missing id, once accessed, threw: {}: {}", ex.getClass().getName(), ex.getMessage());
|
||||
em.getTransaction().rollback();
|
||||
em.close();
|
||||
}
|
||||
|
||||
// ---- Part 2: same-session matrix ----
|
||||
// For each combination, both calls target the SAME id in the SAME session.
|
||||
|
||||
@Test
|
||||
void sessionMatrix_getThenGet_secondCallHitsL1Cache_sameInstance() {
|
||||
Long id = seedBook("Matrix: get/get");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
Book first = session.get(Book.class, id);
|
||||
stats().clear();
|
||||
Book second = session.get(Book.class, id);
|
||||
|
||||
assertThat(stats().getPrepareStatementCount())
|
||||
.as("second get() in the same session must NOT re-fire a SELECT (L1 cache hit)")
|
||||
.isEqualTo(0);
|
||||
assertThat(second).isSameAs(first);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionMatrix_getReferenceThenGetReference_secondCallHitsL1Cache_sameInstance() {
|
||||
Long id = seedBook("Matrix: getReference/getReference");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
Book first = session.getReference(Book.class, id);
|
||||
stats().clear();
|
||||
Book second = session.getReference(Book.class, id);
|
||||
|
||||
assertThat(stats().getPrepareStatementCount())
|
||||
.as("second getReference() in the same session must not fire anything either -- still just a reference")
|
||||
.isEqualTo(0);
|
||||
assertThat(second).isSameAs(first);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionMatrix_getThenGetReference_returnsTheSameAlreadyInitializedInstance() {
|
||||
Long id = seedBook("Matrix: get/getReference");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
Book real = session.get(Book.class, id);
|
||||
stats().clear();
|
||||
Book second = session.getReference(Book.class, id);
|
||||
|
||||
assertThat(stats().getPrepareStatementCount())
|
||||
.as("getReference() after get() must not fire a SELECT -- the real entity is already in the L1 cache")
|
||||
.isEqualTo(0);
|
||||
assertThat(second)
|
||||
.as("getReference() returns the SAME already-managed real instance, not a new proxy, once one exists in this session")
|
||||
.isSameAs(real);
|
||||
assertThat(second.getClass()).isEqualTo(Book.class);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void sessionMatrix_getReferenceThenGet_getReturnsTheExistingProxyAndDoesNotForceInitialization() {
|
||||
Long id = seedBook("Matrix: getReference/get");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
Book proxy = session.getReference(Book.class, id);
|
||||
stats().clear();
|
||||
Book second = session.get(Book.class, id);
|
||||
|
||||
assertThat(second)
|
||||
.as("get() after getReference() returns the SAME proxy already sitting in the L1 cache")
|
||||
.isSameAs(proxy);
|
||||
DEMO.info("get() after getReference(): prepareStatementCount for this call = {}, returned class = {}",
|
||||
stats().getPrepareStatementCount(), second.getClass().getName());
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
// ---- Part 3: proxy identity experiment ----
|
||||
|
||||
@Test
|
||||
void proxyIdentity_instanceofSurvives_equalsDoesNot() {
|
||||
Long id = seedBook("Proxy Identity");
|
||||
EntityManager em1 = emf.createEntityManager();
|
||||
em1.getTransaction().begin();
|
||||
Book real = em1.unwrap(Session.class).get(Book.class, id);
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
em2.getTransaction().begin();
|
||||
Book proxy = em2.unwrap(Session.class).getReference(Book.class, id);
|
||||
|
||||
assertThat(proxy).isInstanceOf(Book.class);
|
||||
assertThat(org.hibernate.Hibernate.getClass(proxy)).isEqualTo(Book.class);
|
||||
assertThat(proxy.getClass()).isNotEqualTo(Book.class);
|
||||
|
||||
// Book does not override equals()/hashCode() -- this is the point of the test.
|
||||
assertThat(real.equals(proxy)).isFalse();
|
||||
assertThat(proxy.equals(real)).isFalse();
|
||||
|
||||
Set<Book> set = new HashSet<>();
|
||||
set.add(real);
|
||||
assertThat(set.contains(proxy))
|
||||
.as("a HashSet built on default equals()/hashCode() cannot recognise the proxy and the real instance as the same row")
|
||||
.isFalse();
|
||||
|
||||
em1.getTransaction().commit();
|
||||
em1.close();
|
||||
em2.getTransaction().commit();
|
||||
em2.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void proxyOutlivesItsSession_throwsLazyInitializationExceptionOnAccess() {
|
||||
Long id = seedBook("Outlives Session");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Book proxy = em.unwrap(Session.class).getReference(Book.class, id);
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
|
||||
assertThrows(LazyInitializationException.class, proxy::getTitle);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
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.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4861. Docs: docs/03-inserting-objects.md.
|
||||
* Run with {@code ./mvnw -Dtest=IdentityBatchTest test}.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class IdentityBatchTest {
|
||||
|
||||
private static final int ROW_COUNT = 30;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void identityGeneratorDisablesBatching_despiteBatchSizeBeingSet() {
|
||||
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
|
||||
Statistics stats = sessionFactory.getStatistics();
|
||||
stats.clear();
|
||||
|
||||
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();
|
||||
|
||||
assertThat(stats.getEntityInsertCount()).isEqualTo(ROW_COUNT);
|
||||
assertThat(stats.getPrepareStatementCount())
|
||||
.as("with GenerationType.IDENTITY, hibernate.jdbc.batch_size has nothing to batch -- "
|
||||
+ "every insert is its own round trip because the generated key is only "
|
||||
+ "known after the row is written")
|
||||
.isEqualTo(ROW_COUNT);
|
||||
}
|
||||
}
|
||||
160
src/test/java/com/ankurm/hibernatedemo/MergeRefreshTest.java
Normal file
160
src/test/java/com/ankurm/hibernatedemo/MergeRefreshTest.java
Normal file
@@ -0,0 +1,160 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.Book;
|
||||
import com.ankurm.hibernatedemo.model.Note;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import org.hibernate.Hibernate;
|
||||
import org.hibernate.Session;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4860. Docs: docs/02-merge-vs-refresh.md.
|
||||
* Run with {@code ./mvnw -Dtest=MergeRefreshTest test}.
|
||||
*
|
||||
* <p>The optimistic-lock-conflict case lives in {@link OptimisticLockTest} instead, since it
|
||||
* needs its own careful narration of exactly when the check fires.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class MergeRefreshTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
private Long seedBook(String title, String status) {
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Book book = new Book(title, "Test Author");
|
||||
book.setStatus(status);
|
||||
em.persist(book);
|
||||
em.getTransaction().commit();
|
||||
Long id = book.getId();
|
||||
em.close();
|
||||
return id;
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeOfDetachedInstance_returnsTheSameManagedInstanceAlreadyInSession() {
|
||||
Long id = seedBook("Managed + Detached", "DRAFT");
|
||||
|
||||
// A separate, already-detached copy of the same row (simulates "the object a controller
|
||||
// method was handed earlier").
|
||||
EntityManager scratch = emf.createEntityManager();
|
||||
scratch.getTransaction().begin();
|
||||
Book detached = scratch.unwrap(Session.class).get(Book.class, id);
|
||||
scratch.getTransaction().commit();
|
||||
scratch.close();
|
||||
detached.setAuthor("Changed On The Detached Copy");
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
// This session already has ITS OWN managed instance for the same row before merge() is
|
||||
// ever called.
|
||||
Book managed = session.get(Book.class, id);
|
||||
|
||||
Book result = session.merge(detached);
|
||||
|
||||
assertThat(result)
|
||||
.as("merge() must return the identity-equal MANAGED instance already tracked by this session, not a new object")
|
||||
.isSameAs(managed);
|
||||
assertThat(result).isNotSameAs(detached);
|
||||
assertThat(managed.getAuthor())
|
||||
.as("the pre-existing managed instance is the one that actually receives the copied state")
|
||||
.isEqualTo("Changed On The Detached Copy");
|
||||
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
void mergeWithCascadeInitializesTheLazyCollectionAnyway() {
|
||||
Long id;
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
Book book = new Book("Lazy Collection Book", "Test Author");
|
||||
seed.persist(book);
|
||||
seed.flush();
|
||||
seed.persist(new Note("first note", book));
|
||||
seed.getTransaction().commit();
|
||||
id = book.getId();
|
||||
seed.close();
|
||||
|
||||
// Load and detach WITHOUT ever touching book.getNotes() -- the collection proxy is never
|
||||
// initialized.
|
||||
EntityManager em1 = emf.createEntityManager();
|
||||
em1.getTransaction().begin();
|
||||
Book detached = em1.unwrap(Session.class).get(Book.class, id);
|
||||
assertThat(Hibernate.isInitialized(detached.getNotes()))
|
||||
.as("sanity check: the collection must still be uninitialized going into detachment")
|
||||
.isFalse();
|
||||
em1.getTransaction().commit();
|
||||
em1.close();
|
||||
|
||||
detached.setAuthor("Edited While Detached");
|
||||
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
em2.getTransaction().begin();
|
||||
Session session = em2.unwrap(Session.class);
|
||||
|
||||
// Going in, the expectation was "merge() doesn't need to touch a collection it was
|
||||
// never asked to load." That's true ONLY when the collection has no CascadeType.MERGE.
|
||||
// Book.notes DOES cascade MERGE (see its Javadoc), and the measured result is the
|
||||
// opposite of the naive expectation: merge() initializes the collection anyway, because
|
||||
// cascading the merge to each element requires knowing what those elements are. Removing
|
||||
// cascade = CascadeType.MERGE from Book.notes and re-running this test flips the result
|
||||
// back to "stays uninitialized" -- confirmed with a throwaway probe before writing this
|
||||
// assertion. See docs/02-merge-vs-refresh.md.
|
||||
Book merged = session.merge(detached);
|
||||
|
||||
assertThat(Hibernate.isInitialized(merged.getNotes()))
|
||||
.as("merge() DOES initialize a LAZY collection when it cascades MERGE to it -- cascading requires traversal")
|
||||
.isTrue();
|
||||
assertThat(merged.getAuthor()).isEqualTo("Edited While Detached");
|
||||
|
||||
em2.getTransaction().commit();
|
||||
em2.close();
|
||||
DEMO.info("merge() initialized the LAZY notes collection because CascadeType.MERGE forces traversal of it");
|
||||
}
|
||||
|
||||
@Test
|
||||
void refreshDiscardsUnflushedEditSilently_noExceptionEver() {
|
||||
Long id = seedBook("Silent Overwrite", "USER_EDIT");
|
||||
|
||||
// Simulate an admin process changing the row out from under the in-memory object.
|
||||
EntityManager admin = emf.createEntityManager();
|
||||
admin.getTransaction().begin();
|
||||
Book row = admin.unwrap(Session.class).get(Book.class, id);
|
||||
row.setStatus("ADMIN_EDIT");
|
||||
admin.getTransaction().commit();
|
||||
admin.close();
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
Book managed = session.get(Book.class, id);
|
||||
assertThat(managed.getStatus()).isEqualTo("ADMIN_EDIT");
|
||||
|
||||
// A local, unflushed edit -- never sent to the database.
|
||||
managed.setStatus("USER_EDIT");
|
||||
assertThat(managed.getStatus()).isEqualTo("USER_EDIT");
|
||||
|
||||
session.refresh(managed);
|
||||
|
||||
assertThat(managed.getStatus())
|
||||
.as("refresh() replaces managed state with the database row -- it does not merge the two; the local edit is simply gone, no exception")
|
||||
.isEqualTo("ADMIN_EDIT");
|
||||
|
||||
em.getTransaction().commit();
|
||||
em.close();
|
||||
}
|
||||
}
|
||||
100
src/test/java/com/ankurm/hibernatedemo/OptimisticLockTest.java
Normal file
100
src/test/java/com/ankurm/hibernatedemo/OptimisticLockTest.java
Normal file
@@ -0,0 +1,100 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
import com.ankurm.hibernatedemo.model.Book;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.OptimisticLockException;
|
||||
import org.hibernate.Session;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4860. Docs: docs/02-merge-vs-refresh.md.
|
||||
* Run with {@code ./mvnw -Dtest=OptimisticLockTest test}.
|
||||
*
|
||||
* <p>The specific thing this test pins down: exactly WHEN the version check fails. It would be
|
||||
* easy to write "merge() throws OptimisticLockException" and leave it there; what actually
|
||||
* happens depends on when Hibernate performs the check relative to the {@code merge()} call, the
|
||||
* flush, and the commit -- and that's worth being precise about rather than assumed.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class OptimisticLockTest {
|
||||
|
||||
private static final Logger DEMO = LoggerFactory.getLogger("DEMO");
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void mergeOfStaleVersionedEntity_throwsOptimisticLockException_andPinsDownWhen() {
|
||||
// Seed.
|
||||
EntityManager seed = emf.createEntityManager();
|
||||
seed.getTransaction().begin();
|
||||
Book book = new Book("Clean Code", "Robert C. Martin");
|
||||
seed.persist(book);
|
||||
seed.getTransaction().commit();
|
||||
Long id = book.getId();
|
||||
seed.close();
|
||||
|
||||
// Detach at version 0.
|
||||
EntityManager loadEm = emf.createEntityManager();
|
||||
loadEm.getTransaction().begin();
|
||||
Book detached = loadEm.unwrap(Session.class).get(Book.class, id);
|
||||
loadEm.getTransaction().commit();
|
||||
loadEm.close();
|
||||
assertThat(detached.getVersion()).isEqualTo(0L);
|
||||
|
||||
// A second, independent transaction advances the row to version 1.
|
||||
EntityManager writer = emf.createEntityManager();
|
||||
writer.getTransaction().begin();
|
||||
Book row = writer.unwrap(Session.class).get(Book.class, id);
|
||||
row.setTitle("Clean Code (2nd Edition)");
|
||||
writer.getTransaction().commit();
|
||||
writer.close();
|
||||
|
||||
// Mutate the STILL version-0 detached instance and attempt to merge it.
|
||||
detached.setAuthor("Robert C. Martin (Uncle Bob)");
|
||||
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
Session session = em.unwrap(Session.class);
|
||||
|
||||
boolean threwDuringMergeCallItself;
|
||||
OptimisticLockException caught = null;
|
||||
try {
|
||||
session.merge(detached);
|
||||
threwDuringMergeCallItself = false;
|
||||
} catch (OptimisticLockException e) {
|
||||
threwDuringMergeCallItself = true;
|
||||
caught = e;
|
||||
}
|
||||
|
||||
if (!threwDuringMergeCallItself) {
|
||||
// merge() itself only queued the state transfer; the version check happens at flush.
|
||||
caught = org.junit.jupiter.api.Assertions.assertThrows(
|
||||
OptimisticLockException.class, () -> em.getTransaction().commit());
|
||||
DEMO.info("OptimisticLockException surfaced at commit()/flush time, NOT from the merge() call itself.");
|
||||
} else {
|
||||
DEMO.info("OptimisticLockException surfaced directly from the merge() call.");
|
||||
em.getTransaction().rollback();
|
||||
}
|
||||
|
||||
assertThat(caught).isNotNull();
|
||||
DEMO.info("exception: {}: {}", caught.getClass().getName(), caught.getMessage());
|
||||
em.close();
|
||||
|
||||
// The other transaction's title change must have survived untouched.
|
||||
EntityManager verify = emf.createEntityManager();
|
||||
verify.getTransaction().begin();
|
||||
Book current = verify.unwrap(Session.class).get(Book.class, id);
|
||||
assertThat(current.getTitle()).isEqualTo("Clean Code (2nd Edition)");
|
||||
assertThat(current.getAuthor()).isEqualTo("Robert C. Martin");
|
||||
verify.getTransaction().commit();
|
||||
verify.close();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.ankurm.hibernatedemo;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
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.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
/**
|
||||
* Backs ankurm.com post 4861. Docs: docs/03-inserting-objects.md.
|
||||
* Run with {@code ./mvnw -Dtest=SequenceBatchTest test}.
|
||||
*
|
||||
* <p>{@link WidgetSequence} sets {@code allocationSize = 25}, matching
|
||||
* {@code hibernate.jdbc.batch_size} in application.yml. See {@code AllocationSizeSweepTest} for
|
||||
* what happens when the two are deliberately mismatched.
|
||||
*/
|
||||
@SpringBootTest
|
||||
class SequenceBatchTest {
|
||||
|
||||
private static final int ROW_COUNT = 30;
|
||||
|
||||
@Autowired
|
||||
private EntityManagerFactory emf;
|
||||
|
||||
@Test
|
||||
void sequenceGeneratorAllowsBatching_fourPreparedStatementsForThirtyRows() {
|
||||
SessionFactory sessionFactory = emf.unwrap(SessionFactory.class);
|
||||
Statistics stats = sessionFactory.getStatistics();
|
||||
stats.clear();
|
||||
|
||||
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();
|
||||
|
||||
assertThat(stats.getEntityInsertCount()).isEqualTo(ROW_COUNT);
|
||||
assertThat(stats.getPrepareStatementCount())
|
||||
.as("30 rows at batch_size=25 is 2 insert batches (25 + 5); allocationSize=25 "
|
||||
+ "means the first 25 ids come from one sequence call and the remaining "
|
||||
+ "5 force a second -- 2 insert batches + 2 sequence calls = 4")
|
||||
.isEqualTo(4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user