Hibernate lets you avoid loading related objects from the database right away — you can pull them in later, when you actually need them. This is convenient, but it's easy to get wrong and end up with either extra queries or a mysterious error. Let's go through both modes and the typical pitfalls.
Two loading modes
When mapping associations with @OneToMany, @ManyToOne, @OneToOne or @ManyToMany, you can specify fetch:
@Entity
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
@OneToMany(mappedBy = "order", fetch = FetchType.LAZY)
private List<OrderItem> items;
}
FetchType.EAGER — the related object is loaded immediately together with the parent, in a single query (or a join).
FetchType.LAZY — the association is not loaded until you first access it. In its place Hibernate substitutes a proxy — an empty stub object that knows only the identifier. The actual SQL runs later, the first time you call any method on that object.
Short formula: LAZY means "load it when asked", EAGER means "load it right away".
How a proxy works
For single-valued associations (@ManyToOne, @OneToOne) Hibernate creates a proxy subclass of your entity. From the outside it looks exactly like a regular object:
Order order = entityManager.find(Order.class, 1L);
// SQL: SELECT * FROM orders WHERE id = 1
// customer is NOT loaded yet
String name = order.getCustomer().getName();
// this is where Hibernate runs a second SELECT * FROM customers WHERE id = ?
For collections (@OneToMany, @ManyToMany), instead of a List or Set Hibernate substitutes its own implementation — PersistentBag, PersistentSet and others. These are also empty until first accessed.
LazyInitializationException: what it is and why it happens
This is the most common error when working with lazy loading:
org.hibernate.LazyInitializationException:
failed to lazily initialize a collection of role: Order.items,
could not initialize proxy - no Session
The cause is that you accessed a lazy association after the Hibernate session was closed. The proxy knows it needs to load the data, but there's no longer an open connection to the database.
A typical scenario in Spring:
// Transaction is open — load the Order
@Transactional
public Order getOrder(Long id) {
return orderRepository.findById(id).orElseThrow();
} // <-- transaction is closed here
// Later, outside the transaction:
Order order = orderService.getOrder(1L);
order.getItems().size(); // BOOM: LazyInitializationException
The Hibernate session lives exactly within the scope of the transaction. Once the @Transactional method finishes, the session is closed and the proxy becomes "dead".
How to fix it properly
1. Load the data you need inside the transaction with JOIN FETCH
The cleanest way is to state explicitly what you want to load, right in the query:
@Query("SELECT o FROM Order o JOIN FETCH o.items WHERE o.id = :id")
Optional<Order> findByIdWithItems(@Param("id") Long id);
Hibernate runs a single SQL with a JOIN and returns a fully initialized object. The lazy association is no longer needed — the data is already there.
2. Load via EntityGraph
An alternative to a JPQL query is @EntityGraph from Spring Data JPA:
@EntityGraph(attributePaths = {"items"})
Optional<Order> findById(Long id);
A flexible option when you want to reuse the same repository method with different sets of associations. More on repositories in the Spring Data JPA article.
3. Access associations inside the transaction
If your business logic needs data from an association, let that happen within the same transaction:
@Transactional
public int countItems(Long orderId) {
Order order = orderRepository.findById(orderId).orElseThrow();
return order.getItems().size(); // OK: the session is still open
}
Why you shouldn't put EAGER everywhere
The first impulse is "I'll set EAGER and forget about the error". That's a trap.
Problem 1: extra data every time. Even when you only need the order's identifier, Hibernate will load all of its items from the database — because EAGER means "always".
Problem 2: N+1 queries or a Cartesian product. If you have a list of 100 orders and each one has an EAGER collection of items, Hibernate will either run 100 additional SELECTs (N+1), or do a JOIN and return rows with duplicates. More on N+1 in the N+1 problem in Hibernate article.
Problem 3: unexpected chains. EAGER on one association can pull an EAGER chain further along the graph, and in the end a single find() loads half the database.
Rule: keep LAZY by default, and specify exactly what you need explicitly in the query.
Reference for JPA defaults:
| Annotation | Default |
|---|---|
@ManyToOne | EAGER |
@OneToOne | EAGER |
@OneToMany | LAZY |
@ManyToMany | LAZY |
@ManyToOne and @OneToOne default to EAGER — a historical accident. It's recommended to change them explicitly to LAZY:
@ManyToOne(fetch = FetchType.LAZY)
private Customer customer;
Open Session in View: don't rely on it
In Spring Boot, Open Session in View (OSIV) is enabled by default — a pattern that keeps the Hibernate session open for the entire duration of the HTTP request, including template rendering.
This lets you access lazy associations even outside @Transactional — there's no error, and it seems like "everything works". But you pay for it with hidden SQL queries in the presentation layer: the controller or template, calling order.getItems(), quietly goes to the database.
To disable OSIV:
spring:
jpa:
open-in-view: false
After disabling it, LazyInitializationException will start showing up wherever the data wasn't loaded explicitly — which is a good thing, because it makes the behavior explicit. Load the associations you need inside the transaction instead of relying on an open session as a "safety net".
In short
FetchType.LAZY— Hibernate substitutes a proxy and loads the data on first access;EAGER— loads it immediately.@ManyToOneand@OneToOnedefault to EAGER — change them to LAZY explicitly.LazyInitializationExceptionhappens when you access a proxy after the session is closed (outside the transaction).- The right fix is to load the associations you need inside the transaction: via
JOIN FETCHin JPQL or@EntityGraph. - Don't put EAGER everywhere: it means hidden extra queries and a risk of N+1.
- OSIV hides the problem but doesn't solve it — disable it and load explicitly.
Further reading
- N+1 problem in Hibernate — how lazy loading in a loop spawns an avalanche of queries and how to detect it.
- Persistence context and the entity lifecycle — how the Hibernate session manages object state.
- Common Hibernate pitfalls — frequent traps when working with an ORM.
- Spring Data JPA — repositories,
@EntityGraphand other abstractions on top of Hibernate.