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:
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
|
||||
Reference in New Issue
Block a user