skip to content

Hibernate's `@Fetch` annotation accepts `FetchMode.SELECT` and `FetchMode.JOIN`. What is the difference, and why does `FetchMode.JOIN` often appear to be ignored?

level: middleimportance: should knowfreq 34%

answer

  1. FetchType = when, FetchMode = how
  2. SELECT: second statement; JOIN: same statement, implies eager
  3. JOIN honoured by find/navigation only
  4. JPQL/HQL/Criteria carry their own fetch plan
  5. Per-query join fetch beats mapping-level JOIN

basics

~20 s

SELECT loads the association with a separate query when it is needed; JOIN loads it in the same statement with an outer join. JOIN applies only when Hibernate builds the SQL itself — find and navigation — and is ignored by JPQL, HQL and Criteria queries, which use their own explicit fetch plan.

solid answer

~50 s

`@Fetch` chooses *how* an association is loaded, which is separate from `FetchType`, which chooses *when*. - `FetchMode.SELECT` — a separate statement, issued when the association is needed. This is the default and the basis of lazy loading and of batching. - `FetchMode.JOIN` — load it in the same statement via a LEFT OUTER JOIN. It also implies eager: there is nothing left to defer once the row is already in hand. - `FetchMode.SUBSELECT` — the collection variant that replays the parent query as a subquery. The surprise is that `FetchMode.JOIN` only takes effect where Hibernate composes the SQL: `em.find`, `getReference` and navigation from a loaded entity. A JPQL/HQL/Criteria query specifies its own select clause and fetch plan, so it ignores `FetchMode.JOIN` completely and falls back to select-style loading — which is exactly how a mapping that "looks eager-joined" still produces N+1 when the entities come from a query. If you want a join for a query, write `join fetch` or use an entity graph.

code

java · 12 lines
java
@Entity
class Order {
    @ManyToOne(fetch = FetchType.LAZY)
    @Fetch(FetchMode.JOIN)
    private Customer customer;
}

// honoured -> single statement with a left outer join
Order o = em.find(Order.class, 1L);

// ignored -> one select for orders, then one per row for customer
List<Order> all = em.createQuery("select o from Order o", Order.class).getResultList();

go deeper

for a junior

State that SELECT uses a separate query and JOIN loads the data in the same statement with an outer join.

for a middle

Separate FetchType from FetchMode, and explain that JOIN applies to find and navigation but is ignored by JPQL, HQL and Criteria.

for a senior

Explain why that asymmetry produces N+1 in list endpoints, and prescribe lazy+SELECT mappings with per-query join fetch or entity graphs, plus batching as the safety net.

for a principal

Frame fetch strategy as a use-case-level decision that must not be frozen into the mapping, and weigh join row multiplication against extra round trips at scale.

## Two orthogonal knobs JPA's `FetchType` (`LAZY` / `EAGER`) says **when** an association is loaded: at the time the owner is loaded, or on first access. Hibernate's `@Fetch(FetchMode.…)` says **how** it is loaded when the moment comes: with a second statement, or folded into the owner's statement. Candidates routinely conflate the two, so being explicit about the split is half the answer. ## The three modes **`FetchMode.SELECT`** issues a separate `select … where fk = ?` when the association is required. It is the default. Combined with `FetchType.LAZY` it is ordinary lazy loading; combined with `FetchType.EAGER` it means the extra query fires immediately after the owner is loaded — the classic secondary-select N+1. **`FetchMode.JOIN`** loads the association in the owner's own statement with a LEFT OUTER JOIN. Because the data arrives with the owner, this mode is inherently eager; declaring `FetchType.LAZY` alongside it does not preserve laziness for the paths where the join applies. One statement is fewer round trips, but joins bring their own costs: a to-many join multiplies the owner's rows by the collection size, so the driver reads and de-duplicates far more result-set rows than the entity count suggests, and joining several collections in one statement multiplies those factors together. **`FetchMode.SUBSELECT`** is the collection-only strategy that replays the parent query as a subquery. ## Why JOIN gets ignored The rule that catches people: `FetchMode.JOIN` influences only the SQL that **Hibernate itself generates from the mapping**. That means: - `em.find(Order.class, id)` — honoured, an outer join appears. - `em.getReference(...)` followed by navigation — honoured on the load. - Navigating from an already-loaded entity to an uninitialised association — honoured. - `em.createQuery("select o from Order o …")`, HQL, Criteria, and named queries — **not** honoured. A JPQL query carries its own fetch plan, expressed by its `join fetch` clauses (and by entity graph hints). Hibernate will not silently add joins the query did not ask for. What it does instead is fall back to select-style loading for those associations — and if the association is `EAGER`, that means one secondary select per returned row. So a mapping annotated `@Fetch(FetchMode.JOIN)` behaves beautifully in a `find`-based unit test and produces N+1 in the list endpoint that uses a query. That contrast is the practical point of the question. The correct tools per situation: - Need the association joined for a specific query: write `join fetch` in the query, or pass an entity graph as a hint. Both are per-query and explicit. - Need to avoid N+1 for lazily-loaded associations generally: use batch fetching (`@BatchSize`, or the global batch fetch size), which works identically no matter how the parents were loaded. - Need one extra query for all parents of a query: consider subselect fetching, with its pagination caveat. ## Why mapping-level eager joins age badly Even where `FetchMode.JOIN` is honoured, encoding it in the mapping makes a global decision for every use case. Some endpoints want the association, most do not; the ones that do not still pay the join. And because the mode implies eagerness, it removes the option of not loading at all. The durable pattern is: map associations lazy and select-mode, then let each query state its own fetch plan. Fetch strategy is a property of the use case, not of the entity. ## A note on `@Fetch` versus `fetch = FetchType` `@Fetch` is a Hibernate annotation; JPA only standardises `FetchType`. Portable code expresses its fetch plans in queries and entity graphs. `@Fetch(FetchMode.SUBSELECT)` and `@BatchSize` have no JPA equivalents, which is a reasonable price for their benefit; `FetchMode.JOIN` has an equivalent (`join fetch`) that is both portable and per-query, so it is the least defensible of the three to put in a mapping. ## Answering crisply "`FetchType` is when, `FetchMode` is how. SELECT means a second statement, JOIN means an outer join in the same statement and implies eager, SUBSELECT is the collection replay. JOIN only applies when Hibernate builds the SQL — `find` and navigation — and is ignored by JPQL/HQL/Criteria, which carry their own fetch plan, so a mapping-level JOIN silently degrades to N+1 in query-driven code paths."

  • If FetchMode.JOIN is ignored by JPQL, how do you get a join for a query?
    Write it explicitly: `join fetch` in JPQL/HQL, `fetch()` on a Criteria join, or pass a `jakarta.persistence.fetchgraph`/`loadgraph` hint with an entity graph. All of these are per-query, so different endpoints can load different shapes of the same entity, which is the behaviour you actually want.
  • Can you keep an association lazy while declaring FetchMode.JOIN?
    Not meaningfully. On the paths where the join applies, the association's data arrives in the same result set as its owner, so there is nothing left to defer — the mode is effectively eager there. That inconsistency (eager via `find`, lazy-and-N+1 via query) is a good argument for leaving mappings lazy with the default SELECT mode and expressing joins per query.

saying these in an interview costs you the question

  • Confusing FetchType (when) with FetchMode (how)
  • Believing @Fetch(FetchMode.JOIN) applies to JPQL and Criteria queries
  • Thinking FetchMode.JOIN can coexist with genuine laziness
  • Assuming FetchMode.SELECT is a performance bug rather than the default
  • Treating a mapping-level fetch strategy as a substitute for per-query fetch plans

context