Fetch Types & Cascading
When you map a JPA relationship, two decisions shape every query and every persist call: when the associated data loads (the fetch type) and which operations propagate from a parent to its children (cascading). Getting these wrong is the single most common source of slow queries and the dreaded LazyInitializationException. This page covers the defaults Hibernate applies, how to override them safely, and how to load associations without leaking the persistence context.
Fetch Types: LAZY vs EAGER
A fetch type tells Hibernate whether to load an association immediately with its owner (EAGER) or to defer loading until the association is first accessed (LAZY). Lazy associations are backed by a proxy — a placeholder object that triggers a SQL query the moment you touch a field.
The critical detail most developers miss is that the defaults differ per relationship type, and the *-to-one defaults are EAGER, which is almost never what you want.
| Relationship | Default Fetch | Recommended |
|---|---|---|
@OneToMany | LAZY | keep LAZY |
@ManyToMany | LAZY | keep LAZY |
@ManyToOne | EAGER | set to LAZY |
@OneToOne | EAGER | set to LAZY |
Warning: An EAGER
@ManyToOnefires an extra SQL join (or a separateSELECT) on every load of the owning entity, even when you never read the association. On collections this is a primary driver of the N+1 select problem.
Override the *-to-one defaults
Always make @ManyToOne and @OneToOne explicitly lazy and fetch them on demand instead.
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
@Entity
public class Order {
@Id
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // override the EAGER default
private Customer customer;
}
CascadeType: Propagating Operations
Cascading propagates an EntityManager operation from a parent entity to its associated children, so you do not have to save or delete each child by hand. You declare it on the relationship via the cascade attribute.
| CascadeType | Propagates | Typical use |
|---|---|---|
PERSIST | em.persist() | save new children with the parent |
MERGE | em.merge() | reattach a detached graph |
REMOVE | em.remove() | delete children when the parent is deleted |
REFRESH | em.refresh() | reload the graph from the database |
DETACH | em.detach() | evict the graph from the context |
ALL | all of the above | strong parent/child ownership |
import jakarta.persistence.CascadeType;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.OneToMany;
import java.util.ArrayList;
import java.util.List;
@Entity
public class Invoice {
@Id
private Long id;
@OneToMany(
mappedBy = "invoice",
cascade = CascadeType.ALL,
orphanRemoval = true
)
private List<LineItem> items = new ArrayList<>();
}
Tip: Cascade
ALLonly when the child truly belongs to the parent and has no independent lifecycle (an order line item, an address). For shared references like aCategorylinked to manyProducts, cascade nothing.
orphanRemoval vs CascadeType.REMOVE
These look similar but trigger on different events:
CascadeType.REMOVEdeletes children only when the parent itself is deleted (em.remove(parent)).orphanRemoval = truedeletes a child the moment it is disassociated from the parent — e.g.invoice.getItems().remove(item). The orphaned row is removed even though the parent still exists.
invoice.getItems().remove(0); // orphanRemoval=true -> DELETE for that row
invoiceRepository.save(invoice);
delete from line_item where id = ?
Note:
orphanRemoval = trueimplies remove-cascade behavior on parent deletion too, so you rarely need bothCascadeType.REMOVEandorphanRemoval.
LazyInitializationException
This exception is thrown when you access a lazy association after the persistence context (Hibernate session) has closed. By default the session is bound to the surrounding transaction, so once your @Transactional service method returns, the proxy can no longer load its data:
org.hibernate.LazyInitializationException: could not initialize proxy
- no Session
It typically happens when a controller serializes an entity returned from a service and Jackson walks into an uninitialized lazy collection — long after the transaction ended.
How to avoid it
Initialize what you need while the session is still open. There are four solid approaches:
- Access inside the transaction — touch the association within the
@Transactionalmethod so it loads before the context closes. See transactions. JOIN FETCHin JPQL — load the association in the same query.@Query("select o from Order o join fetch o.customer where o.id = :id") Optional<Order> findWithCustomer(@Param("id") Long id);@EntityGraph— declaratively fetch named attributes on a repository method.@EntityGraph(attributePaths = "items") Optional<Invoice> findById(Long id);- Map to a DTO before the session closes — project the data inside the transaction and return the DTO, never the entity. See projections.
Warning: Do not reach for
spring.jpa.open-in-view=trueto mask this. Open-Session-in-View keeps the session open for the whole HTTP request, hiding N+1 queries and holding database connections far longer than needed. Prefer explicit fetching.
Putting It Together
A safe default policy: keep every association LAZY, cascade only true parent/child graphs, and fetch what each use case needs with JOIN FETCH or @EntityGraph inside the service transaction.
@Entity
public class Author {
@Id
private Long id;
@OneToMany(mappedBy = "author", cascade = CascadeType.ALL, orphanRemoval = true)
private List<Book> books = new ArrayList<>(); // LAZY by default - good
}
This avoids accidental EAGER loads, deletes orphaned books automatically, and leaves you in control of when the collection is hydrated. For the broader performance picture, study the N+1 problem and how fetch strategy interacts with it.