Skip to content
Spring Boot sb data-jpa 4 min read

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.

RelationshipDefault FetchRecommended
@OneToManyLAZYkeep LAZY
@ManyToManyLAZYkeep LAZY
@ManyToOneEAGERset to LAZY
@OneToOneEAGERset to LAZY

Warning: An EAGER @ManyToOne fires an extra SQL join (or a separate SELECT) 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.

CascadeTypePropagatesTypical use
PERSISTem.persist()save new children with the parent
MERGEem.merge()reattach a detached graph
REMOVEem.remove()delete children when the parent is deleted
REFRESHem.refresh()reload the graph from the database
DETACHem.detach()evict the graph from the context
ALLall of the abovestrong 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 ALL only when the child truly belongs to the parent and has no independent lifecycle (an order line item, an address). For shared references like a Category linked to many Products, cascade nothing.

orphanRemoval vs CascadeType.REMOVE

These look similar but trigger on different events:

  • CascadeType.REMOVE deletes children only when the parent itself is deleted (em.remove(parent)).
  • orphanRemoval = true deletes 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 = true implies remove-cascade behavior on parent deletion too, so you rarely need both CascadeType.REMOVE and orphanRemoval.

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:

  1. Access inside the transaction — touch the association within the @Transactional method so it loads before the context closes. See transactions.
  2. JOIN FETCH in 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);
  3. @EntityGraph — declaratively fetch named attributes on a repository method.
    @EntityGraph(attributePaths = "items")
    Optional<Invoice> findById(Long id);
  4. 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=true to 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.

Last updated June 13, 2026
Was this helpful?