[EN] Hibernate 7 Performance: Solve N+1 problems & use DTO projections

Julian | May 2, 2026 min read

In the first three parts, we mastered the basics, architecture and configuration of Hibernate 7. But in reality, many projects fail not because of the complexity of the API, but because of its performance. Hibernate often has a reputation for being “bloated” or slow. The truth is: most of the time it’s not the framework, but how we feed it.

As a software engineer, performance is not an “add-on” for you, but rather an integral part of the architecture. Today we’ll look at how to eliminate the most common performance killers and minimize database round trips.

1. The N+1 Problem: The Silent Killer

The N+1 problem is the classic misconfiguration problem. It is created when you load a list of entities and Hibernate issues an additional query for each individual entity to load their relations.

The problem: lazy loading without a plan

Imagine loading 50 Post entities, each with an Author. When you iterate over the posts and call post.getAuthor().getName(), Hibernate fires:

  • 1 query for the 50 posts.
  • 50 additional queries for each individual author.

The Solution: Explicit Join Fetches In Hibernate 7 we use JPQL (or the Criteria API) to tell the framework exactly what we want in memory. We already force the join at the database level.

public interface PostRepository extends JpaRepository<Post, Long> {

    // ANTI-PATTERN: Führt zu N+1, wenn Autoren später zugegriffen werden
    @Query("SELECT p FROM Post p")
    List<Post> findAllBasic();

    // SENIOR-SOLUTION: Lädt Post und Author in einer einzigen SQL-Query
    @Query("SELECT p FROM Post p JOIN FETCH p.author WHERE p.status = 'PUBLISHED'")
    List<Post> findAllPublishedWithAuthors();
}

With JOIN FETCH we massively reduce the database round trips - in this case from 51 to exactly 1.


2. DTO Projections: Why entities are often too much of a good thing

A common mistake is to load entire entities for read-only operations (e.g. a search result list). Entities are cumbersome: They end up in the persistence context, are monitored by dirty checking and take up an unnecessary amount of memory.

If you only want to display data, use Java Records and DTO Projections. This is the most efficient way to extract data from Hibernate 7.

// Ein schlanker Record für die UI
public record PostSummary(Long id, String title, String authorName) {}

// Im Repository: Direktes Mapping im JPQL
@Query("SELECT new com.example.dto.PostSummary(p.id, p.title, a.name) " +
       "FROM Post p JOIN p.author a")
List<PostSummary> findSummaries();

Why this is faster:

  • Less data: Only the columns that are in the record are selected (SELECT p.id, p.title... instead of SELECT *).
  • No Overhead: Because records are immutable, Hibernate bypasses first-level cache and dirty checking. This saves CPU cycles and RAM.

3. Batch Inserts: Process mass data efficiently

If you need to import 10,000 records, the standard way (save() in a loop) is disastrous because each call triggers a separate INSERT statement. Hibernate 7 can bundle these statements (batching), which drastically increases the performance of writing processes.

Step A: The configuration

First we need to enable batching in the application.properties:

spring.jpa.properties.hibernate.jdbc.batch_size=50
spring.jpa.properties.hibernate.order_inserts=true
spring.jpa.properties.hibernate.order_updates=true

Step B: The code

Within the loop we have to empty the persistence context regularly so that the main memory does not overflow with thousands of “managed entities”.

@Transactional
public void importLargeAmountOfData(List<DataDTO> dataList) {
    for (int i = 0; i < dataList.size(); i++) {
        MyEntity entity = convertToEntity(dataList.get(i));
        entityManager.persist(entity);

        // Batch-Größe beachten und Speicher freigeben
        if (i > 0 && i % 50 == 0) {
            entityManager.flush();
            entityManager.clear();
        }
    }
}

Conclusion: Measure it or forget it

Performance optimization in Hibernate 7 means one thing above all: SQL control. Use join fetches for relations, access records for read-only data and enable JDBC batching for bulk data.

The most important advice for everyday life: Always turn on SQL logging during development (as described in part 3). If you see hundreds of lines of SQL being logged for a simple list, you’ve found an N+1 problem before it cripples your production.

In the next part of our series, we’re entering the home stretch: We’ll look at how we can successfully migrate an existing application to Hibernate 7 and what pitfalls lurk along the way.