skip to content

JPA defines two query hints for entity graphs, jakarta.persistence.fetchgraph and jakarta.persistence.loadgraph. What is the semantic difference, and when does the choice actually change the SQL?

level: seniorimportance: should knowfreq 40%

answer

  1. fetchgraph = closed world, unlisted -> LAZY
  2. loadgraph = additive, unlisted keeps mapping
  3. Difference only visible with mapped-EAGER attributes
  4. to-one defaults to EAGER — silent difference source
  5. Lazy basics need bytecode enhancement

basics

~20 s

Both make the graph's attributes eager. They differ on everything else: with fetchgraph, attributes not in the graph are lazy regardless of the mapping; with loadgraph, they keep their mapped fetch type, so mapped-EAGER associations still load.

solid answer

~50 s

Both hints treat attributes **named in the graph** as EAGER. The difference is the default for everything else: - **fetchgraph** — the graph is exhaustive. Unlisted attributes are `LAZY`, *overriding the mapping*. A `@ManyToOne(fetch = EAGER)` that you did not list should not be fetched. - **loadgraph** — the graph is additive. Unlisted attributes retain their mapped fetch type, so mapped-EAGER associations are still joined in. The choice only changes SQL when the entity actually has mapped-EAGER attributes — which is exactly why a codebase with everything lazy sees no difference and people conclude the hints are identical. In such a codebase `loadgraph` is the honest choice. Two caveats: making a `@Basic` column lazy requires bytecode enhancement, so `fetchgraph` cannot suppress scalar columns without it; and Hibernate has historically been imperfect about downgrading mapped-EAGER **to-one** associations under `fetchgraph`. Verify against the emitted SQL rather than trusting the spec text.

code

java · 11 lines
java
EntityGraph<Order> g = em.createEntityGraph(Order.class);
g.addAttributeNode("items");

// customer is @ManyToOne(fetch = EAGER) and is NOT in the graph
em.createQuery("select o from Order o", Order.class)
  .setHint("jakarta.persistence.fetchgraph", g)   // intent: no customer join
  .getResultList();

em.createQuery("select o from Order o", Order.class)
  .setHint("jakarta.persistence.loadgraph", g)    // customer still joined
  .getResultList();

go deeper

for a junior

Recall the one-line contrast: fetchgraph makes unlisted attributes lazy, loadgraph leaves them at their mapped fetch type.

for a middle

Add the truth table and note that the difference is only visible when some attribute is mapped EAGER and left out of the graph.

for a senior

Bring the operational angle: to-one defaults to EAGER, Hibernate's inconsistent demotion of eager to-ones, lazy basics needing enhancement, and verifying with SQL logging or query counts.

for a principal

Argue the policy — mappings uniformly LAZY so plans only ever promote, loadgraph as the default hint, and fetch-plan regressions caught by query-count assertions rather than review.

## The two hints JPA 2.1 introduced entity graphs with two attachment modes, expressed as query/`find` hints: ```java query.setHint("jakarta.persistence.fetchgraph", graph); query.setHint("jakarta.persistence.loadgraph", graph); ``` (Under JPA 2.x on the old namespace these were `javax.persistence.fetchgraph` / `javax.persistence.loadgraph`.) The graph object is identical in both cases. Only the interpretation of *absence* differs. ## Fetch graph: closed world A **fetch graph** says: this is the complete set of attributes to load. Anything not in the graph is treated as `FetchType.LAZY` — including attributes the mapping declares `EAGER`. It is a *downgrade-capable* plan. The use case is a read path that must stay narrow: a list endpoint over an entity whose mapping has an unfortunate `@ManyToOne(fetch = EAGER)` on it. You cannot change the mapping without affecting every other caller, but a fetch graph for this query can, in principle, cut that join out. ## Load graph: open world A **load graph** says: load at least these, plus whatever the mapping already says is eager. Unlisted attributes keep their declared fetch type. It is *additive only* — it can promote lazy to eager, never demote eager to lazy. This is the safer and more common choice. It composes predictably with mappings and never surprises you by omitting something another layer of code assumed present. ## When does the difference show up in SQL? Only when the entity has attributes that are **mapped EAGER and not in the graph**. Concretely: | Mapping | In graph? | fetchgraph | loadgraph | |---|---|---|---| | LAZY | yes | joined/fetched | joined/fetched | | LAZY | no | not fetched | not fetched | | EAGER | yes | fetched | fetched | | EAGER | no | **not fetched** | **fetched** | Only the last row differs. In a codebase that follows the standard advice — every association lazy, fetching decided per query — the two hints produce byte-identical SQL. That is why so many teams use them interchangeably and never notice. A related trap: `@ManyToOne` and `@OneToOne` default to `EAGER` in JPA when you do not specify a fetch type. So an entity that *looks* all-lazy because nobody wrote `EAGER` may still have several eager to-ones, and the hint choice does matter there. ## Where the spec and the implementation diverge Two practical limitations to raise if you want to sound like you have used this in anger: 1. **Basic attributes.** Making a scalar column lazy requires the persistent class to be bytecode-enhanced (Hibernate's build-time or runtime enhancement with lazy-loading enabled). Without enhancement, a field is loaded whenever its row is loaded, so a fetch graph cannot suppress it. Graphs are dependable for *associations*, best-effort for basics. 2. **EAGER to-one downgrade.** Hibernate has historically not always honoured the fetchgraph demotion of a mapped-EAGER `@ManyToOne` — the association gets fetched anyway, sometimes via a secondary select rather than a join. Behaviour has varied across 5.x and 6.x. The engineering conclusion is not "the hints are broken" but: **do not rely on fetchgraph to fix a bad mapping; fix the mapping.** Set the association to `LAZY` and let per-query plans do the promoting, which is the direction that always works. ## Practical guidance - Default to **loadgraph**. It expresses "also load these", which is what you almost always mean. - Use **fetchgraph** when you deliberately want a minimal plan and you have verified the emitted SQL for that entity. - Never use either hint as a substitute for correcting an `EAGER` mapping — the mapping is a global default and belongs lazy. - Verify with SQL logging (`hibernate.show_sql` / a statement-count assertion in a test), not by reading the annotation. Fetch-plan bugs are invisible until you count queries. ## Interaction with JOIN FETCH If a query already contains `JOIN FETCH o.items` and you also attach a fetchgraph that omits `items`, you have written two contradictory instructions. Hibernate resolves it in favour of the explicit fetch join, but the code is a landmine for the next reader. One mechanism per association per query.

  • Your team applies fetchgraph everywhere and sees no SQL difference versus loadgraph. What does that tell you about the mappings?
    That no association on those entities is mapped EAGER outside the graph — the two hints only diverge on attributes that are mapped eager and absent from the graph. It usually means the codebase has correctly made every association LAZY, in which case the hint choice is cosmetic and loadgraph is the more honest label.
  • Can a fetch graph make a @Basic column lazy?
    Only with bytecode enhancement enabled for lazy attribute loading. Without it, all scalar columns of a row load together whenever the entity loads, so the graph has no effect on basics. This matters for entities holding a large CLOB or BLOB, where teams often move the column to a separate entity instead of relying on lazy basics.

loadgraph is 'bring these as well'; fetchgraph is 'bring these and nothing else'.

saying these in an interview costs you the question

  • Claiming the two hints are functionally identical without the mapped-EAGER caveat
  • Assuming fetchgraph reliably suppresses a mapped-EAGER @ManyToOne in every Hibernate version
  • Using fetchgraph as a workaround instead of changing the mapping to LAZY
  • Forgetting that @ManyToOne/@OneToOne default to EAGER when no fetch type is written
  • Expecting a fetch graph to stop a large LOB column loading without bytecode enhancement

context