Hibernate Performance Tuning Tips – 2022 Edition

Editors note:
After updating my Hibernate Performance Tuning course in the Persistence Hub, it was time to revisit this article and update it for 2022. It now gives you the best performance tuning tips for Hibernate 4, 5, and 6.


One of the biggest misconceptions about Hibernate is that it causes performance problems if you use it on a huge database or with many parallel users. But that’s not the case. Many successful projects use it to implement a highly scalable and easily maintainable persistence layer. So, what’s the difference between these projects and those suffering from performance problems?

In my consulting projects, I see 2 main mistakes that cause most performance problems:

  1. Checking no or the wrong log messages during development makes it impossible to find potential issues.
  2. Misusing some of Hibernate’s features forces it to execute additional SQL statements, which quickly escalates in production.

In the first section of this article, I will show you a logging configuration that helps you identify performance issues during development. After that, I will show you how to avoid these problems using Hibernate 4, 5, and 6. And if you want to dive deeper into Hibernate and other Java persistence-related topics, I recommend you join the Persistence Hub. It gives you access to a set of exclusive certification courses, expert sessions, and Q&A calls.

1. Find performance issues during development

Finding the performance issues before they cause trouble in production is always the most critical part. But that’s often not as easy as it sounds. Most performance issues are hardly visible on a small test system. They are caused by inefficiencies that scale based on the size of your database and the number of parallel users. Due to that, they have almost no performance impact when running your tests using a small database and only one user. But that changes dramatically as soon as you deploy your application to production.

While the performance issues are hard to find on your test system, you can still see the inefficiencies if you check Hibernate’s internal statistics. One way to do this is to activate Hibernate’s statistics component by setting the system property hibernate.generate_statistics to true and the log level of the org.hibernate.stat category to DEBUG. Hibernate will then collect lots of internal statistics and summarize the most important metrics at the end of each session. For each executed query, it also prints out the statement, its execution time and the number of returned rows.

Here you can see an example of such a summary:

07:03:29,976 DEBUG [org.hibernate.stat.internal.StatisticsImpl] - HHH000117: HQL: SELECT p FROM ChessPlayer p LEFT JOIN FETCH p.gamesWhite LEFT JOIN FETCH p.gamesBlack ORDER BY p.id, time: 10ms, rows: 4
07:03:30,028 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    46700 nanoseconds spent acquiring 1 JDBC connections;
    43700 nanoseconds spent releasing 1 JDBC connections;
    383099 nanoseconds spent preparing 5 JDBC statements;
    11505900 nanoseconds spent executing 4 JDBC statements;
    8895301 nanoseconds spent executing 1 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    26450200 nanoseconds spent executing 1 flushes (flushing a total of 17 entities and 10 collections);
    12322500 nanoseconds spent executing 1 partial-flushes (flushing a total of 1 entities and 1 collections)
}

As you can see in the code snippet, Hibernate tells you how many JDBC statements it executed, if it used JDBC batching, how it used the 2nd level cache, how many flushes it performed, and how long they took.

That shows you which database operations your use case performed. By checking this regularly, you can avoid the most common issues caused by slow queries, too many queries, and missing cache usage. And keep in mind that you are working with a small test database. 5 or 10 additional queries during your test might become several hundred or thousands if you switch to the bigger production database.

If you’re using Hibernate in at least version 5.4.5, you should also configure a threshold for Hibernate’s slow query log. You can do that by configuring the property hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS in your persistence.xml file.

<persistence>
	<persistence-unit name="my-persistence-unit">
		...

		<properties>
			<property name="hibernate.session.events.log.LOG_QUERIES_SLOWER_THAN_MS" value="1" />
			...
		</properties>
	</persistence-unit>
</persistence>

Hibernate then measures the pure execution time of each query and writes a log message for each one that takes longer than the configured threshold.

12:23:20,545 INFO  [org.hibernate.SQL_SLOW] - SlowQuery: 6 milliseconds. SQL: 'select a1_0.id,a1_0.firstName,a1_0.lastName,a1_0.version from Author a1_0'

2. Improve slow queries

Using the previously described configuration, you will regularly find slow queries. But they are not a real JPA or Hibernate issue. This kind of performance problem occurs with every framework, even with plain SQL over JDBC. That’s why your database provides different tools to analyze an SQL statement.

When you’re improving your queries, you might use some database-specific query features. JPQL and the Criteria API do not support these. But don’t worry. You can still use your optimized query with Hibernate. You can execute it as a native query.

Author a = (Author) em.createNativeQuery("SELECT * FROM Author a WHERE a.id = 1", Author.class).getSingleResult();

Hibernate doesn’t parse a native query statement. That enables you to use all SQL and proprietary features your database supports. But it also has a drawback. You get the query result as an Object[] instead of the strongly typed results returned by a JPQL query.

If you want to map the query result to entity objects, you only need to select all columns mapped by your entity and provide its class as the 2nd parameter. Hibernate then automatically applies the entity mapping to your query result. I did that in the previous code snippet.

And if you want to map the result to a different data structure, you either need to map it programmatically or use JPA’s @SqlResultSetMapping annotations. I explained that in great detail in a series of articles:

3. Avoid unnecessary queries – Choose the right FetchType

Another common issue you will find after activating Hibernate’s statistics is the execution of unnecessary queries. This often happens because Hibernate has to initialize an eagerly fetched association, which you do not even use in your business code.

That’s a typical mapping error that defines the wrong FetchType. It is specified in the entity mapping and defines when an association will be loaded from the database. FetchType.LAZY tells your persistence provider to initialize an association when you use it for the first time. This is obviously the most efficient approach. FetchType.EAGER forces Hibernate to initialize the association when instantiating the entity object. In the worst case, this causes an additional query for each association of every fetched entity. Depending on your use case and the size of your database, this can quickly add up to a few hundred additional queries.

To avoid this, you need to change the FetchType of all your to-one associations to FetchType.LAZY. You can do that by setting the fetch attribute on the @ManyToOne or @OneToOne annotation.

@ManyToOne(fetch=FetchType.LAZY)

All to-many associations use FetchType.LAZY by default, and you should not change that.

After you ensured that all your associations use FetchType.LAZY, you should take a closer look at all use cases that use a lazily fetched association to avoid the following performance problem.

4. Avoid unnecessary queries – Use query-specific fetching

As I explained in the previous section, that you should use FetchType.LAZY for all of your associations. That ensures that you only fetch the ones you use in your business code. But if you only change the FetchType, Hibernate uses a separate query to initialize each of these associations. That causes another performance problem that’s called n+1 select issue.

The following code snippet shows a typical example using the Author and Book entity with a lazily fetched many-to-many association between them. The getBooks() method traverses this association.

List<Author> authors = em.createQuery("SELECT a FROM Author a", Author.class).getResultList();
for (Author author : authors) {
	log.info(author + " has written " + author.getBooks().size() + " books.");
}

The JPQL query only gets the Author entity from the database and doesn’t initialize the books association. Because of that, Hibernate needs to execute an additional query when the getBooks() method of each Author entity gets called for the first time. On my small test database, which only contains 11 Author entities, the previous code snippet causes the execution of 12 SQL statements.

12:30:53,705 DEBUG [org.hibernate.SQL] - select a1_0.id,a1_0.firstName,a1_0.lastName,a1_0.version from Author a1_0
12:30:53,731 DEBUG [org.hibernate.stat.internal.StatisticsImpl] - HHH000117: HQL: SELECT a FROM Author a, time: 38ms, rows: 11
12:30:53,739 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,746 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Joshua, lastName: Bloch has written 1 books.
12:30:53,747 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,750 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Gavin, lastName: King has written 1 books.
12:30:53,750 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,753 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Christian, lastName: Bauer has written 1 books.
12:30:53,754 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,756 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Gary, lastName: Gregory has written 1 books.
12:30:53,757 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,759 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Raoul-Gabriel, lastName: Urma has written 1 books.
12:30:53,759 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,762 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Mario, lastName: Fusco has written 1 books.
12:30:53,763 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,764 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Alan, lastName: Mycroft has written 1 books.
12:30:53,765 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,768 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Andrew Lee, lastName: Rubinger has written 2 books.
12:30:53,769 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,771 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Aslak, lastName: Knutsen has written 1 books.
12:30:53,772 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,775 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Bill, lastName: Burke has written 1 books.
12:30:53,775 DEBUG [org.hibernate.SQL] - select b1_0.authorId,b1_1.id,p1_0.id,p1_0.name,p1_0.version,b1_1.publishingDate,b1_1.title,b1_1.version from BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId left join Publisher p1_0 on p1_0.id=b1_1.publisherid where b1_0.authorId=?
12:30:53,777 INFO  [com.thorben.janssen.hibernate.performance.TestIdentifyPerformanceIssues] - Author firstName: Scott, lastName: Oaks has written 1 books.
12:30:53,799 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    37200 nanoseconds spent acquiring 1 JDBC connections;
    23300 nanoseconds spent releasing 1 JDBC connections;
    758803 nanoseconds spent preparing 12 JDBC statements;
    23029401 nanoseconds spent executing 12 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    17618900 nanoseconds spent executing 1 flushes (flushing a total of 20 entities and 26 collections);
    21300 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

You can avoid that by using query-specific eager fetching, which you can define in different ways.

Use a JOIN FETCH clause

You can add a JOIN FETCH clause to your JPQL query. The additional FETCH keyword tells Hibernate to not only join the two entities within the query but to also fetch the associated entities from the database.

List<Author> authors = em.createQuery("SELECT a FROM Author a JOIN FETCH a.books b", Author.class).getResultList();

As you can see in the log output, Hibernate generates an SQL statement that selects all columns mapped by the Author and Book entity and maps the result to managed entity objects.

12:43:02,616 DEBUG [org.hibernate.SQL] - select a1_0.id,b1_0.authorId,b1_1.id,b1_1.publisherid,b1_1.publishingDate,b1_1.title,b1_1.version,a1_0.firstName,a1_0.lastName,a1_0.version from Author a1_0 join (BookAuthor b1_0 join Book b1_1 on b1_1.id=b1_0.bookId) on a1_0.id=b1_0.authorId
12:43:02,650 DEBUG [org.hibernate.stat.internal.StatisticsImpl] - HHH000117: HQL: SELECT a FROM Author a JOIN FETCH a.books b, time: 49ms, rows: 11
12:43:02,667 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    23400 nanoseconds spent acquiring 1 JDBC connections;
    26401 nanoseconds spent releasing 1 JDBC connections;
    157701 nanoseconds spent preparing 1 JDBC statements;
    2950900 nanoseconds spent executing 1 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    13037201 nanoseconds spent executing 1 flushes (flushing a total of 17 entities and 23 collections);
    20499 nanoseconds spent executing 1 partial-flushes (flushing a total of 0 entities and 0 collections)
}

If you’re using Hibernate 4 or 5, you should include the DISTINCT keyword in your query. Otherwise, Hibernate returns each author as often as they have written a book.

And you should also set the query hint hibernate.query.passDistinctThrough to false. That tells Hibernate not to include the DISTINCT keyword in the generated SQL statement and only use it when mapping the query result.

List<Author> authors = em.createQuery("SELECT DISTINCT a FROM Author a JOIN FETCH a.books b", Author.class)
						 .setHint(QueryHints.PASS_DISTINCT_THROUGH, false)
						 .getResultList();

Use a @NamedEntityGraph

Another option is to use a @NamedEntityGraph. This was one of the features introduced in JPA 2.1, and Hibernate has supported it since version 4.3. It allows you to define a graph of entities that shall be fetched from the database.

@NamedEntityGraph(name = "graph.AuthorBooks",  attributeNodes = @NamedAttributeNode(value = "books"))

Combining the entity graph with a query that selects an Author entity gives you the same result as the previous example. The EntityManager fetches all columns mapped by the Author and Book entity and maps them to managed entity objects.

List<Author> authors = em
		.createQuery("SELECT a FROM Author a", Author.class)
		.setHint(QueryHints.JAKARTA_HINT_FETCH_GRAPH, graph)
		.getResultList();

You can find a more detailed description about @NamedEntityGraphs and how to use them to define more complex graphs in JPA Entity Graphs – Part 1: Named entity graphs.

And if you’re using a Hibernate version < 5.3, you should add the DISTINCT keyword and set the query hint hibernate.query.passDistinctThrough to false to let Hibernate remove all duplicates from your query result.

Use an EntityGraph

If you need a more dynamic way to define your entity graph, you can also do this via a Java API. The following code snippet defines the same graph as the previously described annotations.

EntityGraph graph = em.createEntityGraph(Author.class);
Subgraph bookSubGraph = graph.addSubgraph(Author_.books);

List<Author> authors = em
		.createQuery("SELECT a FROM Author a", Author.class)
		.setHint(QueryHints.JAKARTA_HINT_FETCH_GRAPH, graph)
		.getResultList();

Similar to the previous examples, Hibernate will use the graph to define a query the selects all columns mapped by the Author and Book entity and map the query result to the corresponding entity objects.

If you’re using a Hibernate version < 5.3, you should add the DISTINCT keyword and set the query hint hibernate.query.passDistinctThrough to false to let Hibernate remove all duplicates from your query result.

5. Don’t model a Many-to-Many association as a List

Another common mistake that I see in many code reviews is a many-to-many association modeled as a java.util.List. A List might be the most efficient collection type in Java. But unfortunately, Hibernate manages many-to-many associations very inefficiently if you model them as a List. If you add or remove an element, Hibernate removes all elements of the association from the database before it inserts all remaining ones.

Let’s take a look at a simple example. The Book entity models a many-to-many association to the Author entity as a List.

@Entity
public class Book {
	
	@ManyToMany
    private List<Author> authors = new ArrayList<Author>();
	
	...
}

When I add an Author to the List of associated authors, Hibernate deletes all the association records of the given Book and inserts a new record for each element in the List.

Author a = new Author();
a.setId(100L);
a.setFirstName("Thorben");
a.setLastName("Janssen");
em.persist(a);

Book b = em.find(Book.class, 1L);
b.getAuthors().add(a);
14:13:59,430 DEBUG [org.hibernate.SQL] - 
    select
        b1_0.id,
        b1_0.format,
        b1_0.publishingDate,
        b1_0.title,
        b1_0.version 
    from
        Book b1_0 
    where
        b1_0.id=?
14:13:59,478 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        Author
        (firstName, lastName, version, id) 
    values
        (?, ?, ?, ?)
14:13:59,484 DEBUG [org.hibernate.SQL] - 
    update
        Book 
    set
        format=?,
        publishingDate=?,
        title=?,
        version=? 
    where
        id=? 
        and version=?
14:13:59,489 DEBUG [org.hibernate.SQL] - 
    delete 
    from
        book_author 
    where
        book_id=?
14:13:59,491 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        book_author
        (book_id, author_id) 
    values
        (?, ?)
14:13:59,494 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        book_author
        (book_id, author_id) 
    values
        (?, ?)
14:13:59,495 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        book_author
        (book_id, author_id) 
    values
        (?, ?)
14:13:59,499 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        book_author
        (book_id, author_id) 
    values
        (?, ?)
14:13:59,509 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] - Session Metrics {
    26900 nanoseconds spent acquiring 1 JDBC connections;
    35000 nanoseconds spent releasing 1 JDBC connections;
    515400 nanoseconds spent preparing 8 JDBC statements;
    24326800 nanoseconds spent executing 8 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    43404700 nanoseconds spent executing 1 flushes (flushing a total of 6 entities and 5 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

You can easily avoid this inefficiency by modeling your many-to-many association as a java.util.Set.

@Entity
public class Book {
	
	@ManyToMany
    private Set<Author> authors = new HashSet<Author>();
	
	...
}

6. Let the database handle data-heavy operations

OK, this is a recommendation that most Java developers don’t really like because it moves parts of the business logic from the business tier (implemented in Java) into the database.

And don’t get me wrong, there are good reasons to choose Java to implement the business logic and a database to store your data. But you also have to consider that a database handles huge datasets very efficiently. Therefore, it can be a good idea to move not too complex and very data-heavy operations into the database.

There are multiple ways to do that. You can use database functions to perform simple operations in JPQL and native SQL queries. If you need more complex operations, you can call a stored procedure. Since JPA 2.1/Hibernate 4.3, you can call stored procedures via @NamedStoredProcedureQuery or the corresponding Java API. If you’re using an older Hibernate version, you can do the same by writing a native query.

The following code snippet shows a @NamedStoredProcedure definition for the getBooks stored procedure. This procedure returns a REF_CURSOR which can be used to iterate through the returned data set.

@NamedStoredProcedureQuery( 
  name = "getBooks", 
  procedureName = "get_books", 
  resultClasses = Book.class,
  parameters = { @StoredProcedureParameter(mode = ParameterMode.REF_CURSOR, type = void.class) }
)

In your code, you can then instantiate the @NamedStoredProcedureQuery and execute it.

List<Book> books = (List<Book>) em.createNamedStoredProcedureQuery("getBooks").getResultList();

7. Use caches to avoid reading the same data multiple times

Modular application design and parallel user sessions often result in reading the same data multiple times. Obviously, this is an overhead that you should try to avoid. One way to do this is to cache the data that is often read but rarely changed.

As you can see below, Hibernate offers 3 different caches that you can combine with each other.

Caching is a complex topic and can cause severe side effects. That’s why my Hibernate Performance Tuning course (included in the Persistence Hub) contains an entire module about it. In this article, I can only give you a quick overview of Hibernate’s 3 different caches. I recommend you familiarize yourself with all the details of Hibernate’s caches before you start using any of them.

1st Level Cache

The 1st level cache is activated by default and contains all managed entities. These are all entities that you used within the current Session.

2nd Level Cache

The Session-independent 2nd level cache also stores entities. You need to activate it by setting the shared-cache-mode property in your persistence.xml file. I recommend you set it to ENABLE_SELECTIVE and activate caching only for the entity classes that you read at least 9-10 times for each write operation.

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        
        <! –  enable selective 2nd level cache – >
    	<shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
    </persistence-unit>
</persistence>

You can activate caching for an entity class by annotating it with jakarta.persistence.Cacheable or org.hibernate.annotations.Cache.

@Entity
@Cacheable
public class Author { ... }

After you do that, Hibernate automatically adds new Author entities and the ones you fetched from the database to the 2nd level cache. It also checks if the 2nd level cache contains the requested Author entity before it traverses an association or generates an SQL statement for the call of the EntityManager.find method. But please be aware that Hibernate doesn’t use the 2nd level cache if you define your own JPQL, Criteria, or native query.

Query Cache

The query cache is the only one that does not store entities. It caches query results and contains only entity references and scalar values. You need to activate the cache by setting the hibernate.cache.use_query_cache property in the persistence.xml file and set the cacheable property on the Query.

Query<Author> q = session.createQuery("SELECT a FROM Author a WHERE id = :id", Author.class);
q.setParameter("id", 1L);
q.setCacheable(true);
Author a = q.uniqueResult();

8. Perform updates and deletes in bulks

Updating or deleting one entity after the other feels quite natural in Java, but it is also very inefficient. Hibernate creates one SQL query for each entity that was updated or deleted. A better approach would be to perform these operations in bulk by creating update or delete statements that affect multiple records at once.

You can do this via JPQL or SQL statements or by using the CriteriaUpdate and CriteriaDelete operations. The following code snippet shows an example for a CriteriaUpdate statement. As you can see, it is used in a similar way as the already known CriteriaQuery statements.

CriteriaBuilder cb = this.em.getCriteriaBuilder();
  
// create update
CriteriaUpdate<Order> update = cb.createCriteriaUpdate(Order.class);

// set the root class
Root e = update.from(Order.class);

// set update and where clause
update.set("amount", newAmount);
update.where(cb.greaterThanOrEqualTo(e.get("amount"), oldAmount));

// perform update
this.em.createQuery(update).executeUpdate();

Conclusion

As you have seen, there are several Hibernate features you can use to detect and avoid inefficiencies and boost your application’s performance. In my experience, the most important ones are the Hibernate statistics which allow you to find these problems, the definition of the right FetchType in the entity mapping, and query-specific eager fetching.

You can get more information about these and all other Hibernate features in the courses included in the Persistence Hub.

Related Articles

Responses

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

  1. thank you for giving me wonderful information

  2. Thanks lot .. Its really nice tips.

    Regards
    Param

  3. Good, thanks you sir.

  4. useful tips .. good one

  5. There is an ugly catch doing batch update/delete – @(Post|Pre)(Update|Remove) triggers do not get executed!

    1. That’s right. The batch update/delete operations are independent of the entities. So everything that works directly on the entities, like @(Post|Pre)(Update|Remove) triggers or entity caching, are not affected by the update/delete operation.

  6. Hi,

    Great post.
    But I am unable to download pdf as I am already subscribed.
    Is there any other way to download that?

    Thanks,
    Vinod

    1. Thank you, Vinod.
      I send you an email with the link to the pdf and it was also in the mail about this post, which I send this morning.

      Please let me know, if there are any further issues.

      Regards,
      Thorben

  7. Thanks for the tips. These tips are very helpful.

  8. Thanks, nice tips