# 11 — Proxies and LazyInitializationException, verified against Hibernate 7.4.5.Final [← Previous: 10 — Mocking JNDI datasources](10-mocking-jndi-datasources.md) | [Next: 12 — Association mappings →](12-association-mappings.md) Backs [ankurm.com: proxies and the LazyInitializationException](https://ankurm.com/mastering-hibernate-7-proxies-and-the-lazyinitializationexception/). Companion code: [`src/main/java/com/ankurm/hibernatedemo/proxy/`](../src/main/java/com/ankurm/hibernatedemo/proxy/) (entities — [`ProxyBook`](../src/main/java/com/ankurm/hibernatedemo/proxy/ProxyBook.java), [`ProxyPublisher`](../src/main/java/com/ankurm/hibernatedemo/proxy/ProxyPublisher.java), [`ProxyReview`](../src/main/java/com/ankurm/hibernatedemo/proxy/ProxyReview.java)) and [`src/test/java/com/ankurm/hibernatedemo/proxy/`](../src/test/java/com/ankurm/hibernatedemo/proxy/) (six test classes, twelve tests, all green). Raw output: `docs/output/proxy-*.txt`. Environment: Hibernate ORM 7.4.5.Final, Spring Boot 4.1.1, Spring Framework 7.0.9, Jakarta Persistence 3.2.0, JDK 25 (Temurin), H2 2.4.240. This chapter and [chapter 01](01-get-vs-load.md) are the same mechanism seen from two directions: 01 measures what `get()` vs `getReference()` actually do to the persistence context and when a `SELECT` fires; this chapter measures what the *object* `getReference()` hands back actually is, and what breaks when it outlives its session. Read together, they cover the whole proxy lifecycle from creation to `LazyInitializationException`. ## What a proxy actually is, and the two ways to break naive code with it `getReference()` does not hand you back an instance of your entity class. In 7.4.5.Final it hands you a **ByteBuddy-generated nested class named `$HibernateProxy`** — `com.ankurm.hibernatedemo.proxy.ProxyBook$HibernateProxy`, confirmed live in [`docs/output/proxy-lazy-init-and-identity.txt`](output/proxy-lazy-init-and-identity.txt). That is not the Javassist-era naming scheme (`EntityName_$$_javassist_N`) that still circulates in older blog posts and even in some current ones — Hibernate moved its default bytecode provider to ByteBuddy years ago, and 7.4.5 keeps the proxy as a literal static nested class of the entity itself, not a same-package sibling class. Two things follow from that, and both are demonstrated in `ProxyIdentityTest`: - `proxy instanceof ProxyBook` is `true` — the generated class extends your entity. - `proxy.getClass() == ProxyBook.class` is `false`. Any code that branches on `getClass()` instead of `instanceof`, or relies on the JPA-default `equals()`/`hashCode()` (object identity), silently breaks: a `HashSet` containing the real, managed instance does not recognize a proxy for the exact same row as a member (`proxyIdentityTest .naiveEqualsAndHashSet_cannotRecognizeProxyAndRealInstanceAsTheSameRow`, asserted, not claimed). Chapter 01's own proxy-identity experiment reaches the identical conclusion from the `get()`/`getReference()` side — see [`01 — get() vs getReference(), Proxy identity experiment`](01-get-vs-load.md#proxy-identity-experiment). `Hibernate.getClass(proxy)` is the fix — it unwraps to the real entity class regardless of proxy state, and it's asserted to return `ProxyPublisher.class` even for an uninitialized reference. ## The exception, verbatim — and it is not one message, it's two Every blog post on this topic (including the one this replaces) quotes a single wording for `LazyInitializationException`. Running it for real turns up **two different message templates**, depending on whether the frozen thing is a to-one association or a collection: ``` # Collection (ProxyBook.reviews, a LAZY @OneToMany), accessed after session close: Cannot lazily initialize collection of role 'com.ankurm.hibernatedemo.proxy.ProxyBook.reviews' with key '1' (no session) # To-one proxy (a getReference() result), accessed after session close: Could not initialize proxy [com.ankurm.hibernatedemo.proxy.ProxyBook#3] - no session ``` Note the capitalization on the second one too: it's `Could` (capital) and `no session` (lower-case s) — not the commonly-quoted "could not initialize proxy ... no **S**ession". Both are `org.hibernate.LazyInitializationException` (verified: `ex.getClass().getName()` printed and asserted equal to that string), but if you're grepping logs for one exact phrase, you need both. See `LazyInitializationTest` for both reproductions and [`docs/output/proxy-lazy-init-and-identity.txt`](output/proxy-lazy-init-and-identity.txt) for the verbatim capture (also mirrored, unfiltered, in [`docs/output/proxy-lazy-and-identity-run.txt`](output/proxy-lazy-and-identity-run.txt)). ## `Hibernate.initialize()` / `isInitialized()` / `unproxy()` — confirmed against the 7.4.5 jar `javap` against `hibernate-core-7.4.5.Final.jar` (full output in [`docs/output/proxy-settings-javap.txt`](output/proxy-settings-javap.txt)) shows the signatures actually shipped: ``` public static void initialize(java.lang.Object) throws org.hibernate.HibernateException; public static boolean isInitialized(java.lang.Object); public static java.lang.Object unproxy(java.lang.Object); public static T unproxy(T, java.lang.Class); ``` `unproxy()` has **two** overloads, not one — a no-cast version that returns `Object`, and a typed version that takes the target `Class` and returns `T` directly. Both are exercised in `LazyInitializationTest.hibernateUnproxy_...`. A side effect worth knowing: calling `Hibernate.unproxy()` on an uninitialized proxy *initializes it* — `isInitialized()` flips from `false` to `true` as a consequence, not just as a precondition. ## fetchgraph vs. loadgraph, demonstrated, not just stated Every article states the rule ("loadgraph keeps defaults for what's not named; fetchgraph forces everything not named to LAZY") and almost none show it happening. `EntityGraphFetchTest` builds one entity (`ProxyBook`) with two associations of different default fetch types — `publisher` (`@ManyToOne`, default EAGER) and `reviews` (`@OneToMany`, explicit LAZY) — and one named graph that mentions only `reviews`: | Hint | SQL joins | `publisher` after fetch | `reviews` after fetch | |---|---|---|---| | none (plain `find()`) | `left join proxy_publisher` only | initialized (mapped EAGER) | **not** initialized (mapped LAZY) | | `jakarta.persistence.loadgraph` | `left join proxy_publisher` **and** `left join proxy_review` | initialized | initialized | | `jakarta.persistence.fetchgraph` | `left join proxy_review` **only** | **not initialized** (forced to LAZY, despite EAGER mapping) | initialized | The middle column is the whole point: with `fetchgraph`, `book.getPublisher()` comes back as a `ProxyPublisher$HibernateProxy` even though the mapping says EAGER, because fetchgraph treats the graph as the *entire* fetch plan rather than an addition to the mapping's defaults. Verbatim SQL for all three shapes is in [`docs/output/proxy-entitygraph-fetch-vs-load.txt`](output/proxy-entitygraph-fetch-vs-load.txt) (raw run in [`docs/output/proxy-entitygraph-run.txt`](output/proxy-entitygraph-run.txt)). Chapter 12 covers the collection-side counterpart of this same lazy/eager boundary — N+1 counting and `@BatchSize` — see [`12 — Association mappings`](12-association-mappings.md#counting-the-n1). ## `hibernate.enable_lazy_load_no_trans`: still there, still works, now flagged `@Unsafe` This setting has a reputation for being on its way out. It is not gone in 7.4.5.Final — `org.hibernate.cfg.TransactionSettings.ENABLE_LAZY_LOAD_NO_TRANS` still resolves to the string `hibernate.enable_lazy_load_no_trans` (confirmed by `javap`, see [`docs/output/proxy-settings-javap.txt`](output/proxy-settings-javap.txt)). What *is* new-ish and worth a headline: the constant now carries a `@org.hibernate.cfg.Unsafe` marker annotation — an empty marker interface Hibernate's own source uses to flag settings it does not want you reaching for, without actually removing them. `OpenInViewAndLazyLoadNoTransTest` turns it on (`spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true`) and confirms it functionally does what it always did: a to-one proxy, accessed after its originating session and transaction have both closed, initializes successfully instead of throwing — Hibernate opens a temporary session behind the scenes to service exactly that one access. The `@Unsafe` label is Hibernate's own commentary on why you generally shouldn't reach for this, not a sign it's been deprecated or removed. ## OSIV: the default, its warning, and what it actually masks `spring.jpa.open-in-view` defaults to `true` when left unset, and Boot prints a warning at startup that says exactly that — verified verbatim (not paraphrased) against `org.springframework.boot.jpa.autoconfigure.JpaBaseConfiguration$JpaWebConfiguration`'s bytecode: ``` spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning ``` (Note the package: `org.springframework.boot.jpa.autoconfigure`, not `org.springframework.boot .autoconfigure.orm.jpa` — Boot 4 split its autoconfigure module and JPA's auto-configuration now lives in the separate `spring-boot-jpa` artifact.) Proving the *masking* claim required a real HTTP request — OSIV is implemented by `OpenEntityManagerInViewInterceptor`, which only exists for actual servlet requests, so `OsivDefaultWarningTest` and `OsivDisabledExceptionTest` spin up a real embedded Tomcat (`@SpringBootTest(webEnvironment = RANDOM_PORT)`) with one controller ([`OsivBookController`](../src/test/java/com/ankurm/hibernatedemo/proxy/OsivBookController.java)/[`OsivBookService`](../src/test/java/com/ankurm/hibernatedemo/proxy/OsivBookService.java)) that returns a `ProxyBook` entity directly and lets Jackson serialize `getReviews()` on the response-writing thread — deliberately not a DTO, because the DTO is the fix, and the point here is what happens without it. Same code, same data, only the property differs: ``` # open-in-view left unset (Boot default true): GET /osiv/books/1 200 {"title":"OSIV Default Book","publisher":{...},"id":1,"reviews":[{"comment":"Rendered fine","id":1}]} # open-in-view=false: GET /osiv/books/2 (identical entity graph, identical controller) 500 {"timestamp":"...","status":500,"error":"Internal Server Error","path":"/osiv/books/2"} ``` Server-side log for the second case (captured via `CapturedOutput`, not inferred): ``` Resolved [org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: Cannot lazily initialize collection of role 'com.ankurm.hibernatedemo.proxy.ProxyBook.reviews' with key '2' (no session)] ``` Worth calling out as its own gotcha: the HTTP error *body* Boot's default `BasicErrorController` returns is a bare `{"timestamp":...,"status":500,...}` even with `server.error.include-message=always` set — the failure happens inside the Jackson `HttpMessageConverter` while writing the response, which Boot reports as a generic 500 rather than routing the original exception's message into the error attributes map. If you only look at the HTTP response, you will not learn why it failed; you have to check the application log, which is exactly what real incident response looks like for this failure mode. Full sequence in [`docs/output/proxy-osiv-and-no-trans.txt`](output/proxy-osiv-and-no-trans.txt) (raw run in [`docs/output/proxy-osiv-run.txt`](output/proxy-osiv-run.txt)). ## Bytecode enhancement (`hibernate-enhance-maven-plugin`) — not run here The article draft this replaces implies bytecode enhancement changes proxy behavior in ways worth demonstrating. Two settings for it are real and confirmed present in 7.4.5.Final — `org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_LAZY_INITIALIZATION`, `ENHANCER_ENABLE_DIRTY_TRACKING`, and `ENHANCER_ENABLE_ASSOCIATION_MANAGEMENT` all exist ([`docs/output/proxy-settings-javap.txt`](output/proxy-settings-javap.txt)). Wiring up the actual Maven plugin to enhance classes at build time and diff the resulting behavior was not attempted: a quick check for `org.hibernate.orm.tooling:hibernate-enhance-maven-plugin:7.4.5.Final` and `org.hibernate.orm:hibernate-enhance-maven-plugin:7.4.5.Final` on Maven Central both came back "could not be resolved" in this sandbox. Per the brief's own guidance, this looked like a rabbit hole rather than a quick add — flagging it honestly rather than guessing at what enhancement would do differently here. ## Testcontainers, Docker, and the honest gap Not applicable to this chapter directly (see [chapter 09](09-testing-in-memory-databases.md) for that), but it's worth noting here too: everything above ran against a single shared H2 in-memory database in a sandboxed container. No Docker was available, so nothing here was cross-checked against a real Postgres/MySQL proxy-and-lazy-loading story — the LazyInitializationException and proxy identity mechanics are Hibernate-internal and database-agnostic, but that claim itself rests on understanding of Hibernate's architecture rather than an executed cross-database test. [← Previous: 10 — Mocking JNDI datasources](10-mocking-jndi-datasources.md) | [Next: 12 — Association mappings →](12-association-mappings.md)