🛠️ Testing, Tools & Ecosystem · Advanced

JPA & Hibernate concepts in Java

Entities, persistence context, lazy loading, the N+1 query problem.

🧩 The mysteryA page lists 100 blog posts. It's fast on your laptop, but in production it fires 101 SQL queries per request. Nobody wrote a loop of queries... or did they?

Objects mapped to tables

JPA (Jakarta Persistence) maps classes to tables with annotations like **@Entity and @Id. Hibernate is its most popular implementation. Queries are written in JPQL**, against entities and fields rather than tables.

@Entity
class Post {
    @Id Long id;
    String title;
    @ManyToOne(fetch = FetchType.LAZY)
    Author author;
}

The persistence context

An EntityManager's persistence context tracks every managed entity it loaded: a first-level cache. At flush or commit, dirty checking compares each entity with its snapshot and writes the needed UPDATEs automatically.

🤔 Think first

No save() call?

Inside a transaction you load a post and call post.setTitle("New"), but never call persist() or merge(). Is the change saved?

Think about it, then reveal the answer

Yes. The post is managed, so dirty checking notices the change and issues an UPDATE at flush/commit. No explicit save needed.

Lazy vs eager

A LAZY relation is loaded on first access, through a proxy. An EAGER one is loaded right away with its owner. Lazy is usually the right default, but it hides a trap when you loop.

🔮 Predict it

Count the queries

There are 100 posts and Post.author is LAZY. How many SQL queries can this run?

List<Post> posts = em.createQuery(
        "SELECT p FROM Post p", Post.class)
    .getResultList();
for (Post p : posts) {
    show(p.getAuthor().getName());
}
  1. 1
  2. 2
  3. Up to 101
Show the answer

Up to 101: one query loads the posts, then each author is loaded with its own query the first time it's touched. This is the classic N+1 problem.

Fixing N+1

✗ Lazy loads in a loop
SELECT p FROM Post p
-- then 1 query per author

1 + N round trips to the database.

✓ JOIN FETCH
SELECT p FROM Post p
JOIN FETCH p.author

Posts and authors in one query. Entity graphs or batch fetching work too.

⚠️ The trap

LazyInitializationException

A controller calls post.getComments() after the transaction has ended. The entity is now detached, so its lazy proxy can't reach the database: LazyInitializationException. Fetch what you need in the query, or map to a DTO inside the transaction. Making everything EAGER just loads too much everywhere else.

💼 In the real world

Fast in dev, slow in prod

With 10 rows locally, N+1 is invisible; with 10,000 in production, it's an outage. Turn on SQL logging (or statistics) in development and count the queries per request: if it grows with the data, look for a lazy relation inside a loop.

Key takeaways

  1. Persistence context = tracked entities + first-level cache
  2. Dirty checking saves changes without an explicit save()
  3. N+1: 1 query for the list + 1 per row for a lazy relation
  4. Fix with JOIN FETCH, entity graphs or batch fetching
🤯 Did you know?

JPQL talks about entities, not tables: SELECT p FROM Post p works even if the table behind Post is called blog_posts.

Practice questions

There are 100 posts and Post.author is LAZY. How many SQL queries can this run?

List<Post> posts = em.createQuery(
        "SELECT p FROM Post p", Post.class)
    .getResultList();
for (Post p : posts) {
    show(p.getAuthor().getName());
}
  1. 1
  2. 2
  3. Up to 101
  4. Exactly 100
Check your answer

Up to 101. One query loads the posts, then each distinct author is loaded with its own query the first time it's touched: the classic N+1 problem.

What's the best fix for that N+1 problem?

  1. Call em.clear() inside the loop
  2. Add an index on posts.id
  3. Fetch authors together: SELECT p FROM Post p JOIN FETCH p.author
  4. Make every relation in the model EAGER
Check your answer

Fetch authors together: SELECT p FROM Post p JOIN FETCH p.author. JOIN FETCH loads posts and their authors in one query. Global EAGER fetching tends to load too much data everywhere else.

Who opens that transaction and creates the EntityManager for you? Next: Spring Boot's container.