Hibernate Second Level Cache for Dummies

As a student or intern, you'll probably come across a similar task – enabling entity caching to save on database calls. However, on the Internet, you'll have to piece together information from ten-year-old articles, stackoverflow questions, and documentation.

This tutorial aims to simplify this task and show you step-by-step how to set up a basic cache in Hibernate 6.

However, the author recommends first reading this article with the theory, although at the time of writing it was already 12 years old:)

First steps

As an example, let's create a small application with a menu table. It is not updated often, so we want to visit the database as rarely as possible.

Entity code

Table of items:

@Getter
@Setter
@Entity
@Table(name = "menu_items")
public class MenuItemEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "calories", nullable = false)
    private Integer calories;

    @Column(name = "price", nullable = false)
    private Double price;

}

Let's make 3 requests to the menu table to get an entity with id = 1. Success!

{
    "id": 1,
    "name": "bread",
    "calories": 500,
    "price": 50.0
}

Noo…

SQL query logs to the database

SQL query logs to the database

As you can see, each request required a call to the database. It makes sense – we haven't set up any cache yet!

Configuring the Second Level Cache

Hibernate does not provide an L2 cache implementation out of the box – you need to connect any of the available implementations. The most popular ones are described Here. We will use ehcache.

Let's add the following dependencies to pom.xml:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.4.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-jcache</artifactId>
            <version>6.4.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.10.0</version>
            <classifier>jakarta</classifier>
        </dependency>

And let's change the application.yml (application.properties) file to let Hibernate know that we want to enable the L2 cache and specify its configuration:

  jpa:
    properties:
      hibernate:
        javax.cache:
          provider: org.ehcache.jsr107.EhcacheCachingProvider
          uri: ehcache.xml
        cache:
          use_second_level_cache: true
          region.factory_class: jcache
  • use_second_level_cache – enables level 2 cache

  • provider — specifies the cache provider (implementation) class

  • uri – points to the configuration file

  • region.factory_class – encapsulates the implementation details of the provider

Now let's configure ehcache.xml. A full description of all parameters can be found in documentationbut we only need 3 – name, lifetime and heap size. For the convenience of further reuse, we will set the parameters as a template, which we will then apply to the MenuItemEntity cache:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns="http://www.ehcache.org/v3"
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">

    <cache-template name="default">
        <expiry>
            <ttl unit="days">1</ttl>
        </expiry>
        <heap>1000</heap>
    </cache-template>

    <cache alias="com.example.cache_for_dummies.entity.MenuItemEntity" uses-template="default"/>

</config>

The last touch is to mark the MenuItemEntity entity as cacheable – for this, an annotation is enough org.hibernate.annotations.Cache:

@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)

You can specify one of the following strategies as the usage parameter:

  • READ-ONLY – good for data that is frequently read but not modified;

  • READ-WRITE – suitable for applications that regularly update data;

  • NONSTRICT READ-WRITE – suitable if the data is not updated very often, and it is unlikely that two transactions will simultaneously change the same entity;

  • TRANSACTIONAL — support for transactional providers. Used in JTA environment.

In addition to the mandatory usage, you can specify other parameters in the @Cache annotation:

Name

Type

Description

includeLazy

boolean

Determines whether lazy attributes will be included in the second-level cache when they are loaded. Defaults to true

region

String

Cache region. Defaults to the fully qualified class name.

We launch the application and make the same 3 requests to the menu:

Only one request was made to the database.

Only one request was made to the database.

There was only one request to the database, after which the data was taken from the cache – hooray! Now let's try to do getAll, that is, get all the records from the menu:

Oops!

Oops!

Even though we have configured an entity cache, each selection is accompanied by a query to the database.

Why is this happening?

The thing is that entities are cached by default by their identifier, i.e. id. Hibernate does not store the objects themselves – it stores their string representation in a format conceptually represented as a key-value. So, roughly speaking, when we want to “find all”, hibernate has no data about what all is, and goes to the database for this.

To change this, you will need to enable query cache. For this:

  1. Let's enable the query cache in application.properties / application.yml:

cache:
  use_query_cache: true
  1. Let's configure the query cache in ehcache.xml (in this case, we'll apply a template):

<cache alias="org.hibernate.cache.internal.StandardQueryCache"
       uses-template="default"/>
  1. Let's mark the necessary methods in the repository as cacheable. In our case, this is findAll:

public interface MenuItemRepository extends JpaRepository<MenuItemEntity, Long> {

    @Override
    @QueryHints(@QueryHint(name = AvailableHints.HINT_CACHEABLE, value = "true"))
    List<MenuItemEntity> findAll();

}

Done! In the logs we see only one request to the database:

About collections and foreign keys

The restaurant is gaining popularity, so it was decided to open several branches – and store information about them in the database. In addition, the menu in restaurants can be different, and we want to get information at any time about which restaurants you can try this or that dish. The updated entities take the following form:

Code

New entity RestaurantEntity:

@Getter
@Setter
@Entity
@Table(name = "restaurants")
public class RestaurantEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @ManyToMany
    @JoinTable(name = "restaurants_items",
            joinColumns = @JoinColumn(name = "restaurant_id"),
            inverseJoinColumns = @JoinColumn(name = "item_id"))
    private List<MenuItemEntity> menu;

}

Updated MenuItemEntity entity:

@Getter
@Setter
@Entity
@Table(name = "menu_items")
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
public class MenuItemEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", nullable = false)
    private Long id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "calories", nullable = false)
    private Integer calories;

    @Column(name = "price", nullable = false)
    private Double price;

    @ManyToMany(mappedBy = "restaurants")
    private List<RestaurantEntity> restaurants;

}

We rejoice and launch the application:

{
    "id": 2,
    "name": "beer",
    "calories": 400,
    "price": 350.0,
    "restaurants": [
        {
            "id": 2,
            "name": "Restaurant 2"
        }
    ]
}

We look at the logs and see that, although the menu_items table was accessed only once, the restaurants_items table is required for each request.

And again, calls to MenuItemEntity end with queries to the database...

And again, calls to MenuItemEntity end with queries to the database…

The thing is that collections are not cached automatically – you need to explicitly specify this using the same @Cache annotation:

@ManyToMany(mappedBy = "menu")
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY)
private List<RestaurantEntity> restaurants;

The problem is solved – now the connection with the restaurants_items table is also in the cache 🙂 In the case of a One-To-Many relationship, this would be enough, but for Many-To-Many, an additional call is required directly to the restaurants table, skipping the selection from restaurants_items. But this can also be avoided by hanging the @Cacheable annotation on the RestaurantEntity entity:

@Getter
@Setter
@Entity
@Table(name = "restaurants")
@Cacheable
public class RestaurantEntity {
Voila!

Voila!

It is sometimes considered good practice to mark entities with both @Cache and @Cacheable at the same time.

In this case, a separate cache will be created for com.example.cache_for_dummies.entity.MenuItemEntity.restaurants by default – so you should either remember to configure it in ehcache.xml, or explicitly specify the region. For example:

@ManyToMany(mappedBy = "menu")
@Cache(usage = CacheConcurrencyStrategy.READ_ONLY,
        region = "com.example.cache_for_dummies.entity.MenuItemEntity")
private List<RestaurantEntity> restaurants;

Unfortunately, it doesn't work the other way around:

{
    "id": 2,
    "name": "Restaurant 2",
    "menu": [
        {
            "id": 2,
            "name": "beer",
            "calories": 400,
            "price": 350.0
        },
        {
            "id": 4,
            "name": "ribs",
            "calories": 700,
            "price": 800.0
        }
    ]
}
The restaurants values ​​are not cached, and the menu_items values ​​are fetched using a join rather than a separate query.

The restaurants values ​​are not cached, and the menu_items values ​​are fetched using a join rather than a separate query.

⚠️ It is important to remember that if you hang the @Cache annotation on the List menu, it will work, but the cache will also be created for the RestaurantEntity entity itself!


I hope this article has clarified the basics of setting up a second-level cache in Hibernate. The full code for the program can be found here Here.

Similar Posts

Leave a Reply

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