Add hibernate-demo: get() vs load(), merge() vs refresh(), inserting objects (Hibernate 7.4.1.Final + Spring Boot 4.1.0)
This commit is contained in:
98
docs/01-get-vs-load.md
Normal file
98
docs/01-get-vs-load.md
Normal file
@@ -0,0 +1,98 @@
|
||||
# 01 — get() vs load() / getReference()
|
||||
|
||||
[← Previous: 00 — Versions](00-versions.md) | [Next: 02 — merge() vs refresh() →](02-merge-vs-refresh.md)
|
||||
|
||||
Backs [ankurm.com: Hibernate 7 — get() vs load()](https://ankurm.com/hibernate-7-get-vs-load-which-one-should-you-actually-use/).
|
||||
|
||||
Source: [`GetVsLoadRunner`](../src/main/java/com/ankurm/hibernatedemo/scenario/GetVsLoadRunner.java).
|
||||
Run it yourself with `./scripts/run.sh getvsload`; the full transcript below is committed at
|
||||
[`docs/output/get-vs-load.txt`](output/get-vs-load.txt).
|
||||
|
||||
## What the six steps show
|
||||
|
||||
1. **`session.get()` on an existing id** — one `SELECT` immediately, returns the real entity.
|
||||
2. **`session.get()` on a missing id** — one `SELECT`, returns `null`. No exception, ever.
|
||||
3. **`session.getReference()` on an existing id** — no `SELECT` at the call site. The `SELECT`
|
||||
only happens the moment a non-identifier accessor (`getTitle()`) is called on the returned
|
||||
proxy. The log ordering in the transcript is the actual evidence for this, not a claim about
|
||||
it — the `about to call` / `now calling` markers in the code bracket exactly where each
|
||||
`SELECT` does or doesn't appear.
|
||||
4. **`session.getReference()` on a missing id** — same deferral: the proxy is handed back with
|
||||
no exception, and only accessing it throws
|
||||
`jakarta.persistence.EntityNotFoundException: No row with the given identifier exists`.
|
||||
5. **A proxy outlives its session** — `getReference()`, then commit and close, then access:
|
||||
`org.hibernate.LazyInitializationException: Could not initialize proxy [...] - no session`.
|
||||
6. **Proxy identity** — `Book.class.isInstance(proxy)` is `true`, but `real.getClass() ==
|
||||
proxy.getClass()` is `false` (the proxy's runtime class is `Book$HibernateProxy`), and
|
||||
`real.equals(proxy)` is `false` even though both represent the same row.
|
||||
|
||||
## What surprised me building this
|
||||
|
||||
Step 6 is the one worth sitting with. `Book` here does not override `equals()`, so
|
||||
`real.equals(proxy)` falling through to reference equality is not a Hibernate quirk — it's plain
|
||||
Java doing exactly what an un-overridden `equals()` always does. But it means the moment `get()`
|
||||
and `getReference()` are mixed for the *same row* in code that ever compares entities by
|
||||
`equals()` (a `Set`, a `List.contains()`, a manual reconciliation), the proxy boundary silently
|
||||
breaks that comparison. `instanceof` survives it. `equals()`, `hashCode()`-based collections, and
|
||||
naive `==` do not. The fix is the one every Effective-Java-style guide already recommends —
|
||||
implement `equals()`/`hashCode()` off the identifier, never off the object's identity — but this
|
||||
is what makes skipping that advice actually bite: not a compile error, a `false` you don't
|
||||
notice until two supposedly-identical entities land in the same `HashSet` and both show up.
|
||||
|
||||
Also unexpected going in: `getReference()` on a *missing* row throws `EntityNotFoundException`
|
||||
on first non-id access, not `ObjectNotFoundException`. The two names get used interchangeably in
|
||||
older Hibernate discussion; running it pins down which one this exact version actually throws
|
||||
(see the transcript for the full class name and message).
|
||||
|
||||
## Full captured transcript
|
||||
|
||||
```console
|
||||
Hibernate: create global temporary table HTE_book(rn_ integer not null, id bigint, version bigint, author varchar(255), title varchar(255), primary key (rn_)) transactional
|
||||
Hibernate: create global temporary table HTE_widget_sequence(rn_ integer not null, id bigint, name varchar(255), primary key (rn_)) transactional
|
||||
Hibernate: create table book (id bigint not null, author varchar(255), title varchar(255), version bigint not null, primary key (id))
|
||||
Hibernate: create table widget_identity (id bigint generated by default as identity, name varchar(255), primary key (id))
|
||||
Hibernate: create table widget_sequence (id bigint not null, name varchar(255), primary key (id))
|
||||
Hibernate: create sequence book_seq start with 1 increment by 50
|
||||
Hibernate: create sequence widget_seq start with 1 increment by 25
|
||||
Hibernate: select next value for book_seq
|
||||
Hibernate: /* insert for com.ankurm.hibernatedemo.model.Book */insert into book (author,title,version,id) values (?,?,?,?)
|
||||
binding parameter (1:VARCHAR) <- [Joshua Bloch]
|
||||
binding parameter (2:VARCHAR) <- [Effective Java]
|
||||
binding parameter (3:BIGINT) <- [0]
|
||||
binding parameter (4:BIGINT) <- [1]
|
||||
SEED: inserted Book id=1
|
||||
--- Step 1: session.get() on an existing id ---
|
||||
about to call session.get(Book.class, 1)
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
get() returned: Book{id=1, title=Effective Java, author=Joshua Bloch, version=0}
|
||||
--- Step 2: session.get() on a missing id ---
|
||||
about to call session.get(Book.class, 999001)
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [999001]
|
||||
get() returned: null (no exception thrown)
|
||||
--- Step 3: session.getReference() on an existing id ---
|
||||
getReference() returned proxy of class com.ankurm.hibernatedemo.model.Book$HibernateProxy -- no SELECT above this line
|
||||
now calling proxy.getTitle() ...
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
getTitle() returned 'Effective Java' -- the SELECT for this ran just above this line
|
||||
--- Step 4: session.getReference() on a missing id ---
|
||||
getReference() returned a proxy for a row that does not exist -- no exception yet: com.ankurm.hibernatedemo.model.Book$HibernateProxy
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [999001]
|
||||
accessing the proxy threw jakarta.persistence.EntityNotFoundException: No row with the given identifier exists for entity [com.ankurm.hibernatedemo.model.Book with id '999001']
|
||||
--- Step 5: proxy accessed after its session is closed ---
|
||||
session closed. proxy in hand: com.ankurm.hibernatedemo.model.Book$HibernateProxy
|
||||
accessing the proxy after close threw org.hibernate.LazyInitializationException: Could not initialize proxy [com.ankurm.hibernatedemo.model.Book#1] - no session
|
||||
--- Step 6: proxy identity vs a real loaded instance ---
|
||||
Hibernate: select b1_0.id,b1_0.author,b1_0.title,b1_0.version from book b1_0 where b1_0.id=?
|
||||
binding parameter (1:BIGINT) <- [1]
|
||||
real.getClass() = com.ankurm.hibernatedemo.model.Book
|
||||
proxy.getClass() = com.ankurm.hibernatedemo.model.Book$HibernateProxy
|
||||
proxy instanceof Book.class: true
|
||||
real.getClass() == proxy.getClass(): false
|
||||
real.equals(proxy) before proxy access: false
|
||||
```
|
||||
|
||||
[← Previous: 00 — Versions](00-versions.md) | [Next: 02 — merge() vs refresh() →](02-merge-vs-refresh.md)
|
||||
Reference in New Issue
Block a user