skip to content

Code fails because a lazily mapped association is touched after the unit of work that loaded it has already finished. Rank the realistic fixes — loading the data inside the transaction, using a fetch join or entity graph, and projecting straight into a DTO — and explain how you choose between them.

level: seniorimportance: must knowfreq 60%

answer

  1. Fetch more or read less — nothing else
  2. DTO projection removes proxies by construction
  3. join fetch / entity graph when you need entities
  4. Initialize-in-transaction = plan hidden in code
  5. EAGER and no-trans loading = global tax

basics

~20 s

Fix it at the query, not at the point of failure. Best: select exactly the fields you need into a DTO. Next: load the entity with a fetch join or entity graph so the needed associations arrive in one query. Acceptable: touch what you need while the context is open. Never: force loading after the fact.

solid answer

~50 s

The failure is a mismatch between what the query fetched and what the consumer reads, so every good fix changes the **read**. 1. **DTO projection** — if the consumer only needs a few fields, write a query that selects them (`select new dto(...)` or a tuple/interface projection). No proxies exist, so the problem cannot recur, and you move less data. 2. **Fetch join / entity graph** — if you genuinely need managed entities with a subgraph, state the graph in the query (`join fetch`, or a named/dynamic entity graph). One statement, deterministic plan, and it also fixes the N+1 that lazy access would have caused. 3. **Touch inside the transaction** — calling `Hibernate.initialize(...)` or reading the association before the boundary. It works, but the fetch plan lives in imperative code far from the query and drifts easily; use it for one-off cases. What I reject: widening the mapping to `EAGER`, and enabling loading outside a transaction. Both fix the symptom globally and cost you everywhere.

code

java · 14 lines
java
// 1. projection - no entities, no proxies
List<OrderRow> rows = em.createQuery(
    "select new com.acme.OrderRow(o.id, o.placedAt, c.name) " +
    "from Order o join o.customer c where o.status = :s", OrderRow.class)
  .setParameter("s", OPEN).getResultList();

// 2. entity + declared graph
Order o = em.createQuery(
    "select o from Order o join fetch o.customer where o.id = :id", Order.class)
  .setParameter("id", id).getSingleResult();

// 3. initialize before leaving the transaction
Order o2 = em.find(Order.class, id);
Hibernate.initialize(o2.getLines());

go deeper

for a junior

Know the two directions of the fix — fetch what you need in the query, or select only the fields you need — and that touching the association after the transaction ends is not an option.

for a middle

Be able to write both the fetch join and the projection query, and explain why EAGER is not the right lever. Mention the bag/pagination caveats of fetch joins.

for a senior

Rank the options and justify the ranking by where the fetch plan lives and what it costs at scale; connect the fix to the N+1 it also removes, and back it with statement-count tests.

for a principal

Set the house rule: which layers may hold entities, that fetch plans are declared per use case, and how it is enforced in review and tests, rather than fixing sites one at a time.

## The real diagnosis The exception is not "a Hibernate quirk". It is Hibernate telling you that the query you ran fetched less than the code downstream reads. There are only two honest cures: fetch more, or read less. Everything else is a way to hide the mismatch. So rank fixes by **where they put the decision**. Good fixes put it in the query, where it is visible, testable and reviewable. Bad fixes push it into global configuration or into runtime behaviour that nobody can see at the call site. ## 1. Project into a DTO (usually best) Most read paths — a list screen, an API response, an export — need a handful of columns, not a managed object graph. Selecting those directly removes the problem by construction: ```java select new com.acme.OrderRow(o.id, o.placedAt, c.name) from Order o join o.customer c where o.status = :status ``` Benefits beyond the exception: no proxies to explode later, no dirty checking or snapshots for objects you never intend to modify, far less memory and network traffic, and the query states exactly what the screen needs, so changing the screen forces changing the query. The cost is a class per shape and the fact that projections are read-only — if the caller must mutate and persist, this is the wrong tool. ## 2. Fetch join or entity graph (best when you need entities) When you really need managed entities — you will modify them, or the domain logic lives on them — declare the graph you need at load time: ```java select distinct o from Order o join fetch o.customer left join fetch o.lines where o.id = :id ``` or the same thing expressed as an entity graph passed as a hint, which composes better when the same query is reused with different graphs. Either way the association arrives initialized, one round trip, deterministic. This also eliminates the N+1 you would otherwise have caused, so it is a performance fix as much as a correctness fix. Two cautions. Joining several collections multiplies rows and, for `List`-mapped bags, Hibernate rejects it outright, so fetch one collection per query and get the others with a second query or batch fetching. And do not fetch-join a collection together with database-side pagination — the row multiplication makes limit/offset meaningless and Hibernate falls back to paginating in memory. ## 3. Initialize inside the transaction (acceptable, limited) ```java @Transactional public OrderView load(long id) { Order o = em.find(Order.class, id); Hibernate.initialize(o.getLines()); // or just read them return map(o); } ``` This works and is honest about intent. Its weakness is that the fetch plan is now imperative code sitting away from the query; when someone adds a field to the mapper months later, nothing reminds them to initialize it, and the extra selects are hidden. It also still issues one query per association. Fine as a local fix, poor as a house style. ## What not to do - **Switch the mapping to `EAGER`.** It fixes this call site by penalising every other one: every query on that entity now drags the association along, including queries that never touch it, and eager collections quietly become N+1 in list queries. Fetch strategy is a per-query decision; the mapping should stay lazy. - **Enable loading outside the transaction** (`hibernate.enable_lazy_load_no_trans`). It converts a loud error into silent, unbounded, out-of-transaction queries. - **Catch the exception** or null-check around it. That is data loss dressed as robustness. - **Return entities from an API layer and let a serializer decide the graph.** Whatever the serializer touches becomes your fetch plan, which is how one added field turns into thousands of queries. ## Choosing, in practice Ask two questions. *Does the caller mutate the objects?* If no, project. If yes, keep entities and declare a graph. *Is the graph shape stable per use case?* If yes, name the entity graph or write the specific query; if a single query must serve several shapes, prefer several small queries over one that fetches everything for everyone. For collections that are needed sometimes and are small, `@BatchSize` or subselect fetching is a reasonable middle ground: it does not prevent the exception on detached objects, but it turns the in-transaction access pattern from N+1 into a handful of statements. Finally, make it enforceable: assert in tests that a service returns fully-formed results (or DTOs) rather than relying on the caller's context being open, and count statements in tests for hot paths.

  • Why is switching the mapping to EAGER a bad global fix?
    Fetch strategy is a per-query concern; the mapping applies to every query. Making the association eager forces every load of that entity — including list queries and queries that never touch the association — to pay for it, and eager collections in list queries degenerate into N+1 selects. The lazy mapping plus per-query fetch joins keeps the cost where the need is.
  • When would you deliberately keep entities rather than projecting to DTOs?
    When the caller mutates the objects and relies on dirty checking, or when meaningful domain behaviour lives on the entity and duplicating it over a DTO would split the logic. In those cases keep the entity and declare the graph you need with a fetch join or entity graph, so the fetch plan is still explicit.
  • You need one entity with two collections initialized. What is the safe way?
    Do not fetch-join both in one query: the cartesian product multiplies rows, and with `List`-mapped bags Hibernate raises MultipleBagFetchException. Fetch one collection in the main query and load the second with a separate query in the same transaction, or rely on batch/subselect fetching for the second.

saying these in an interview costs you the question

  • Proposing EAGER as the fix and treating fetch type as a mapping-time rather than a query-time decision.
  • Fetch-joining several collections in one query and expecting correct row counts.
  • Combining a collection fetch join with setMaxResults and assuming the database paginates.
  • Catching the exception, or null-checking around the association, and calling that handled.
  • Saying "the DTO is just extra boilerplate" without acknowledging it removes proxies, snapshots and over-fetching.

context