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…
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:
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:
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:
Let's enable the query cache in application.properties / application.yml:
cache:
use_query_cache: true
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"/>
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.
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 {
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
}
]
}
⚠️ It is important to remember that if you hang the @Cache annotation on the List
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.