How to Configure Level 2 Cache in JPA Applications

In this updated article we will learn how to configure Hibernate Second Level Caches with Jakarta Persistence API to optimize database interactions. The primary objective is to minimize the frequency of database hits by strategically implementing caching mechanisms for both entities and queries within the context of the Jakarta Persistence API. Besides, we will also learn how to monitor Caches using WildFly management tools.

Hibernate first-level cache

  • Scope: The first-level cache is associated with the Hibernate session. It is sometimes referred to as the session cache because it is specific to the current Hibernate session.
  • Lifetime: The first-level cache exists only for the duration of the session. When you close the Hibernate session, this Cache is no longer valid.
  • Purpose: Allows to reduce redundant database queries within the same session. When you retrieve an entity during a session, it is stored in the first-level cache. Subsequent requests for the same entity within the same session will bypass the Database and go through the Cache.

Let’s see an example :

 Session session = getSessionFactory().openSession();
 Transaction tx = session.beginTransaction();
 User user1 = (User) session.load(User.class, 1L);
 System.out.println(user1.getName());
 User user2 = (User) session.load(User.class, 1L);   
 System.out.println(user2.getName());       
 tx.commit();
 session.close();

Here, we are issuing two session.load to retrieve the User object using its primary key. Here only the first query hits the database.

You can verify it by adding the following property to your persistence.xml, which displays the SQL execution on the DB:

<property name="hibernate.show_sql" value="true" />

Hibernate second-level cache

  • Scope: Unlike the first-level cache, the second-level cache is shared among multiple sessions. It is a global cache that you can use to store data across different sessions.
  • Lifetime: The second-level cache persists beyond the lifespan of a session and is typically configured to work across the entire application.
  • Purpose: The primary goal of the second-level cache is to reduce the number of database hits across different sessions. It is especially useful in scenarios where multiple sessions or even different parts of an application need to access the same data.

Besides, we must differentiate between the Entity Cache and the Query Cache.

  • The Entity Cache allows caching entire entities, representing objects mapped to database records. It allows Hibernate to store and retrieve instances of entity classes without repeatedly querying the database.
  • The Query Cache allows caching the results of queries, storing the identifiers of the entities that match a particular query. It helps avoid re-executing the same query and retrieving the same set of results from the database multiple times.

In both cases, you must be aware that these caches are not on by default but it needs some configuration in your persistence.xml to tell Hibernate to turn on the caches. Let’s see how to configure them in the next section.

How to configure the Second Level Cache in your applications

In order to configure the Second Level Cache we need to set the correct Hibernate properties in our persistence.xml file. Here is an example persistence.xml that you can include in your application to activate the Second Level Cache and the Query Cache:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.0"
    xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
        http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd">
    <persistence-unit name="primary">
        <jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
        <shared-cache-mode>ENABLE_SELECTIVE</shared-cache-mode>
        <properties>
            <property name="hibernate.cache.use_second_level_cache" value="true" />
            <property name="hibernate.cache.use_query_cache" value="true" />

            <property name="hibernate.show_sql" value="true" />
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.generate_statistics" value="true" />
        </properties>
    </persistence-unit>
</persistence>

In this configuration hibernate.cache.use_second_level_cache focuses on caching entire entities to reduce database hits, while hibernate.cache.use_query_cache is deals with caching query results to optimize repeated query executions.

Notice the shared-cache-mode element which you can use to configure the Second Level Cache behaviour.

  • DISABLE_SELECTIVE, Enables caching for all entities while excluding those with the annotation Cacheable(false).
  • ENABLE_SELECTIVE: Enables caching for all entities with the Cacheable(true) annotation, while excluding caching for all other entities.
  • ALL, the shared cache is globally enabled for all entities in the persistence unit.
  • NONE: Disables the second level cache behavior. In this case the JPA provider must not cache any entity in the second-level cache.

If you dont’ specify the mode, it will apply the JPA provider specific second-level cache defaults.

A Second Level Cache Example

After discussing the theory, let’s see a practical example of how to configure the second level Cache in an application running on WildFly. The following Class contains both the Cachable annotation to cache the Entity and a Query Hint to Cache a Query:

@Entity
@Cacheable
@NamedQueries(
{
@NamedQuery(
name = "Customers.findAll",
query = "SELECT c FROM Customer c ORDER BY c.id",
hints = { @QueryHint(name = "org.hibernate.cacheable", value =
"true") }  
)
})

 
public class Customer {
    @Id
    @SequenceGenerator(
            name = "customerSequence",
            sequenceName = "customerId_seq",
            allocationSize = 1,
            initialValue = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "customerSequence")
    private Long id;
    @Column(length = 40)
    private String name;
    @Column(length = 40)
    private String surname;

   // Getters and Setters omitted for brevity

}

We will not go in detail in the above Entity Class. If you are new to Jakarta Persistence API, we recommend checking this tutorial: HelloWorld JPA application

In order to manage the above Entity Class, we will use the following Service Class which contains the CRUD methods to perform Database Operations:

public class CustomerRepository {

	@PersistenceContext
	private EntityManager entityManager;

	public List<Customer> findAll() {
		return entityManager.createNamedQuery("Customers.findAll", Customer.class)
				.getResultList();
	}

	public Customer findCustomerById(Long id) {

		Customer customer = entityManager.find(Customer.class, id);

		if (customer == null) {
			throw new WebApplicationException("Customer with id of " + id + " does not exist.", 404);
		}
		return customer;
	}
	@Transactional
	public void updateCustomer(Customer customer) {

		Customer customerToUpdate = findCustomerById(customer.getId());
		customerToUpdate.setName(customer.getName());
		customerToUpdate.setSurname(customer.getSurname());
	}
	@Transactional
	public void createCustomer(Customer customer) {

		entityManager.persist(customer);

	}
	@Transactional
	public void deleteCustomer(Long customerId) {

		Customer c = findCustomerById(customerId);
		entityManager.remove(c);
	}

}

You can check the full source code for this example at the bottom of this article. We will now deploy and test the Caching of our Entities and Queries.

Monitoring the Second Level Cache

Our application includes the WildFly Maven plugin, therefore you can deploy it as follows:

mvn install wildfly:deploy

For example, let’s add some Entities in the Database:

curl -X POST http://localhost:8080/2lcache-demo/rest/customers  -H 'Content-Type: application/json' -d '{"name":"John","surname":"Smith"}'

Then, execute a findAll through the following GET:

curl http://localhost:8080/2lcache-demo/rest/customers

As you can see from the Console, which outputs the Hibernate statistics, the first Query will hit the Database:

09:40:10,220 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] (default task-1) Session Metrics {
    127519 nanoseconds spent acquiring 1 JDBC connections;
    71633 nanoseconds spent releasing 1 JDBC connections;
    652414 nanoseconds spent preparing 1 JDBC statements;
    295902 nanoseconds spent executing 1 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    1237362 nanoseconds spent performing 4 L2C puts;
    0 nanoseconds spent performing 0 L2C hits;
    41228 nanoseconds spent performing 1 L2C misses;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

On the other hand, by re-executing the Query will not hit again the Database:

09:40:12,465 INFO  [org.hibernate.engine.internal.StatisticalLoggingSessionEventListener] (default task-1) Session Metrics {
    0 nanoseconds spent acquiring 0 JDBC connections;
    0 nanoseconds spent releasing 0 JDBC connections;
    0 nanoseconds spent preparing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC statements;
    0 nanoseconds spent executing 0 JDBC batches;
    0 nanoseconds spent performing 0 L2C puts;
    291037 nanoseconds spent performing 2 L2C hits;
    0 nanoseconds spent performing 0 L2C misses;
    0 nanoseconds spent executing 0 flushes (flushing a total of 0 entities and 0 collections);
    0 nanoseconds spent executing 0 partial-flushes (flushing a total of 0 entities and 0 collections)
}

You can verify the Status of your Cache through the Management Console of WildFly. Firslty access the Runtime Attributes of the JPA subsystem and select your application.

Second Level Cache configuration step-by-step tutorial

Click on View. Then, verify in the Tabs of your Persistence Units the Statistics for your Second Level Cache:

Second Level Cache configuration on WildFly

Besides, you can also check the Cache Statistics with the Command Line Interface by reading the Runtime Statistics of the Persistence Unit of your application:

/deployment=2lcache-demo.war/subsystem=jpa/hibernate-persistence-unit=2lcache-demo.war#primary:read-resource(include-runtime=true)
  "result" => {
. . . . .
        "hibernate-persistence-unit" => "2lcache-demo.war#primary",
        "optimistic-failure-count" => 0L,
        "prepared-statement-count" => 3L,
        "query-cache-hit-count" => 1L,
        "query-cache-miss-count" => 1L,
        "query-cache-put-count" => 1L,
        "query-execution-count" => 1L,
        "query-execution-max-time" => 14L,
        "query-execution-max-time-query-string" => "SELECT c FROM Customer c ORDER BY c.id",

Finally, we will mention that it is also possible to access the individual caches with the CLI. Therefore, we can access the Entity Cache as follows:

/deployment=2lcache-demo.war/subsystem=jpa/hibernate-persistence-unit=2lcache-demo.war#primary/entity-cache=com.mastertheboss.model.Customer:read-resource(include-runtime=true)

Then, we can access the Query Cache as follows:

/deployment=2lcache-demo.war/subsystem=jpa/hibernate-persistence-unit=2lcache-demo.war#primary/query-cache=SELECT_space_c_space_FROM_space_Customer_space_c_space_ORDER_space_BY_space_c.id:read-resource(include-runtime=true)

When to use the second-level Cache

Even if the second-level cache can reduce database round trips since entities are retrieved from the cache rather than from the database, there are other options to achieve the same goal. Therefore, you should consider these alternatives before jumping headlong in the second-level cache layer:

  • Tune the Database cache so that your working set fits into memory. This will greatly reduce Disk I/O traffic.
  • Optimize the SQL statements through JDBC batching, statement caching, indexing can reduce the average response time, therefore increasing throughput as well.
  • Database replication should be taken into account to increase read-only transaction throughput

After properly tuning the database, to further reduce the response time while increasing the throughput, application-level caching should be added.

Conclusion

By following the best practices outlined in this article, developers can fine-tune the second-level cache settings on WildFly, striking a balance between memory usage, data consistency, and application performance. Harnessing the power of the second-level cache not only contributes to a more responsive application but also minimizes the load on the underlying database, ultimately leading to an optimized and efficient persistence layer.

Source code for this article: https://github.com/fmarchioni/mastertheboss/tree/master/jpa/2lcache

Found the article helpful? if so please follow us on Socials