Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~9 min· topic 4 of 5

Topic 8.4

JPA and Hibernate: Mapping, Fetching and the N+1 Problem

In one line

JPA maps entities to tables and relationships to foreign keys, but default fetching can silently issue one query per row (N+1). Make associations LAZY, load what each use case needs with fetch joins, entity graphs or batch fetching, use DTO projections for reads, and apply @Version for optimistic locking.

0/5 · 0%

Think of it like this

A waiter taking an order for a table of 20 by walking to the kitchen once per person instead of once for the whole table. Each trip is quick; 21 trips are not.

Key ideas

  1. 01

    Mappings: @ManyToOne (FK on this side), @OneToMany(mappedBy) (inverse side), @OneToOne, @ManyToMany (prefer an explicit join entity when the relationship has attributes). Owning side = the side with the FK.

  2. 02

    Defaults: @ManyToOne and @OneToOne are EAGER by default in JPA, which is a common source of hidden queries. Set fetch = FetchType.LAZY everywhere and fetch explicitly per use case.

  3. 03

    N+1: loading 100 orders and then touching order.getCustomer().getName() triggers 100 more queries. Detect with SQL logging or Hibernate statistics in tests (assert query counts).

  4. 04

    Fixes: JOIN FETCH in JPQL (one query, but don't fetch two collections at once, which creates a Cartesian product); @EntityGraph(attributePaths = ...) on repository methods; @BatchSize / hibernate.default_batch_fetch_size = 50 (loads lazy associations in batches with IN lists); DTO projections for read-only screens.

  5. 05

    Locking: @Version gives optimistic locking (OptimisticLockException on conflict); @Lock(LockModeType.PESSIMISTIC_WRITE) issues SELECT ... FOR UPDATE.

  6. 06

    Batch writes: enable hibernate.jdbc.batch_size and order_inserts. Note that GenerationType.IDENTITY disables insert batching; use a sequence with allocationSize (pooled optimizer).

Code & diagrams

Order.javajava
@Entity
@Table(name = "orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
    @SequenceGenerator(name = "order_seq", sequenceName = "order_seq", allocationSize = 50)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY, optional = false)
    @JoinColumn(name = "customer_id")
    private Customer customer;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<OrderItem> items = new ArrayList<>();

    @Version
    private long version;
}

public interface OrderRepository extends JpaRepository<Order, Long> {
    // one query: orders + customers
    @Query("select o from Order o join fetch o.customer where o.createdAt >= :since")
    List<Order> findRecentWithCustomer(Instant since);

    // read model: no entities, no dirty checking
    @Query("select new com.shop.OrderRow(o.id, c.name, o.total) from Order o join o.customer c where c.id = :cid")
    List<OrderRow> rowsForCustomer(Long cid);
}
n-plus-one.logtext
-- before: 1 + 100 queries
select o.id, o.customer_id, ... from orders o where o.created_at >= ?
select c.id, c.name from customer c where c.id = ?     -- x100

-- after join fetch: 1 query
select o.id, ..., c.id, c.name from orders o join customer c on c.id = o.customer_id
where o.created_at >= ?

Interview problem

The problem

An order list page issuing 1,201 queries

An order history page shows 200 orders with customer name, item count and shipping status. The logs show 1,201 queries per page load. Fix it.

When it breaks

JOIN FETCH on two collections with pagination

What you see

Hibernate warns HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory and loads every row into memory before paging, or throws MultipleBagFetchException.

Fix & prevent

Page on IDs first, then fetch associations for those IDs; use batch fetching for collections.

Explain it without notes

01

Why should to-one associations be LAZY by default?

Practice

01

Enable Hibernate SQL logging, reproduce an N+1, then fix it with an @EntityGraph.

Trade-offs

  • ↔

    ORMs speed up development and handle mapping; they hide SQL, so query counts and plans must be checked deliberately.

Done when you can

  • I can map relationships correctly and eliminate N+1 with fetch joins, entity graphs, batching or projections.