Hibernate Cache

Berat Yesbek
10 min readMar 5, 2024

--

Hibernate Cache, Hibernate Cache Levels and Custom Cache with Redis.

Greetings, everyone; today, we will discuss the Hibernate Cache and Custom Cache. Let’s refresh our coffees and get started! ☕ 📰😊

Caching Story

Alice is a product manager at an Engineering Company that creates customer business solutions. Having a project released that a customer wants, they confront some issues considering the response time performance. Having had this feedback, Alice directly contacted the engineering team.

Alice: Hey, Our customers reported there are some performance issues. Do we have a chance to control everything because they want a quick solution?

Engineer Team: Yes, we will check it. Some data changes are less frequent, such as Countries, Cities, Universities, etc., and should have been retrieved from the Cache instead of the Relational Database. We might consider this solution to increase the performance of the customer's application.

Alice: Okay! I will report this to our customer once everything is done.

In this example, caching will be a great solution because some data changes are less frequent. Caching is a technique that helps us take data from the database once and put it all into the memory, then retrieve it from the memory when clients need it. This steadily enhances response performance time. Generally, each ORM tool consists of a Caching mechanism of its own. In this article, we will be considering two approaches: one of mine and another of Hibernate caching.

Before jumping into our own caching mechanism, let’s begin by writing our own caching mechanism. However, I want to warn you regarding this issue. This code might not work very well. I want to provide a viewpoint.

Custom Cache Mechanism with Redis

In this example, we will use Redis to store our data and Aspect to run some business logic before and after the actual method. Therefore, let’s start by making some configuration for our Redis.

Cache Aspect WorkFlow

When a client calls a method annotated with the `CacheAspect` annotation, it is intercepted by an aspect named `CacheAspectImpl`. This aspect then checks if the requested query already exists in the cache. If it does, the aspect returns the cached result. If the query does not exist in the cache, the aspect caches the result retrieved from the database and then returns it.

FULL_CODE (Click)

RedisConfig

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String redisHost;

@Value("${spring.data.redis.port}")
private int redisPort;

@Bean
public LettuceConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(new RedisStandaloneConfiguration(redisHost, redisPort));
}

@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}

RedisRepository

import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;

import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.TimeUnit;

@Repository
@RequiredArgsConstructor
public class RedisCacheRepository {
private HashOperations<String, String, Object> hashOperations;
private final RedisTemplate<String, Object> redisTemplate;

@PostConstruct
private void init() {
hashOperations = redisTemplate.opsForHash();
}

public void save(String key, String hashKey, Object cacheData, Long ttl) {
hashOperations.put(key, hashKey, cacheData);
if (ttl != null) {
redisTemplate.expire(key, ttl, TimeUnit.MINUTES);
}
}

public List<Object> findAll(String key) {
return hashOperations.values(key);
}

public Optional<Object> findByHashKey(String key, String hashKey) {
return Optional.ofNullable(hashOperations.get(key, hashKey));
}

public void delete(String key, String hashKey) {
hashOperations.delete(key, hashKey);
}

public void deleteAllKeysWithPrefix(String keyPrefix) {
List<String> otpCountKeys = findKeysByPattern(keyPrefix);
for (String otpCountKey : otpCountKeys) {
redisTemplate.delete(otpCountKey);
}
}

public void saveAsPair(String key, Object value, Long ttl) {
redisTemplate.opsForValue().set(key, value);
if (ttl != null) {
redisTemplate.expire(key, ttl, TimeUnit.MINUTES);
}
}

private List<String> findKeysByPattern(String pattern) {
return Objects.requireNonNull(redisTemplate.keys(pattern)).stream().toList();
}
}

Cache Aspect

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheAspect {
String key();

long duration() default 60;
}

CacheAspectImpl


import com.beratyesbek.youtubehibernate.repository.redis.RedisCacheRepository;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;
import java.util.Optional;
import java.util.logging.Logger;

@Aspect
@Configuration
@RequiredArgsConstructor
public class CacheAspectImpl {

private final RedisCacheRepository redisCacheRepository;
private final Logger logger = Logger.getLogger(CacheAspectImpl.class.getName());
@Around("@annotation(CacheAspect)")
public Object before(ProceedingJoinPoint joinPoint) throws Throwable {
// Get method signature using reflection
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
CacheAspect cacheAspect = method.getAnnotation(CacheAspect.class);

// Get Key from annotation
String key = cacheAspect.key();

// Get hash code of method arguments to use as hash key because each key should be unique.
// If request comes with same arguments, it should return from cache.
String argsHashCode = getArgsHashCode(joinPoint.getArgs());

// Check if cache has the key , 64073954-, 2115594645-
Optional<Object> object = redisCacheRepository.findByHashKey(key, argsHashCode);

// If cache has the key, return the value from cache
if (object.isPresent()) {
logger.info("Retrieved from the cache");
return object.get();
}

// If cache does not have the key, proceed the method and save the result to cache
Object result = joinPoint.proceed();
redisCacheRepository.save(key, argsHashCode, result, cacheAspect.duration());
return result;
}

private String getArgsHashCode(Object[] args) {
StringBuilder stringBuilder = new StringBuilder();
for (Object arg : args) {
stringBuilder.append(arg != null ? arg.hashCode() : "null");
stringBuilder.append("-");
}
return stringBuilder.toString();
}

The idea is straightforward: before the actual method runs, check the key that comes from some arguments; if it exists, then return it; if it does not exist, then cache it and return it. However, what if the data is changed? What should we do? The idea is simple: we have to remove the previously cached values and wait for another new request that comes from the client side. When the client sends a new request, `CacheAspect` retrieves the data from the database and stores it in Redis’ cache. To handle this scenario, we need a `CacheRemoveAspect` that, when data is changed, removes previously cached values by prefix key.

Delete Workflow

Cache Remove Aspect

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CacheRemoveAspect {
String[] key();
}

CacheRemoveAspectImpl

import com.beratyesbek.youtubehibernate.repository.redis.RedisCacheRepository;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.context.annotation.Configuration;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.logging.Logger;

@Aspect
@Configuration
@RequiredArgsConstructor
public class CacheRemoveAspectImpl {

private final RedisCacheRepository repository;
private final Logger logger = Logger.getLogger(CacheRemoveAspectImpl.class.getName());


@After("@annotation(CacheRemoveAspect)")
public void after(JoinPoint joinPoint) {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
CacheRemoveAspect cacheRemoveAspect = method.getAnnotation(CacheRemoveAspect.class);
Arrays.stream(cacheRemoveAspect.key()).forEach(repository::deleteAllKeysWithPrefix);
logger.info("Cache is removed");
}
}

Usage of these two approach

import com.beratyesbek.youtubehibernate.core.CacheAspect;
import com.beratyesbek.youtubehibernate.core.CacheRemoveAspect;
import com.beratyesbek.youtubehibernate.entity.Merchant;
import com.beratyesbek.youtubehibernate.repository.MerchantRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class MerchantService {
private final MerchantRepository merchantRepository;

@CacheAspect(key = "MerchantService.findAll", duration = 100)
public List<Merchant> findAll() {
return merchantRepository.findAll();
}

@CacheRemoveAspect(key = {"MerchantService.findAll", "MerchantService.findByName"})
public Merchant save(Merchant merchant) {
return merchantRepository.save(merchant);
}

@CacheAspect(key = "MerchantService.findByName", duration = 100)
public List<Merchant> findByName(String name) {
return merchantRepository.findByName(name);
}

}

This is a simple custom approach to the caching mechanism that I came up with myself. While this approach may not be the most useful solution, I wanted to share my perspective on caching and the use of aspect mechanisms. Caching is a cross-cutting concern that should ideally be managed by aspects.

Test

    @Bean
public CommandLineRunner runMerchant(MerchantService merchantService) {
return args -> {

logger.info("Merchant is called first time");
List<Merchant> merchantsFirstTime = merchantService.findByName("Berat");

logger.info("Merchant is called second time");
List<Merchant> merchantsSecondTime = merchantService.findByName("Berat");

logger.info("Merchant is saved");
Merchant merchant = Merchant.builder().name("Berat").email("berat@gmail.com").address("Istanbul").build();
merchantService.save(merchant);

logger.info("Merchant is called third time");
List<Merchant> merchantsThirdTime = merchantService.findByName("Berat");

logger.info("Merchant is called fourth time");
List<Merchant> merchantsFourthTime = merchantService.findByName("Furkan");
};
}

Output

c.b.y.YoutubeHibernateApplication: Merchant is called first time
org.hibernate.SQL: select m1_0.id,m1_0.address,m1_0.email,m1_0.name from merchant m1_0

c.b.y.YoutubeHibernateApplication: Merchant is called second time
c.b.y.core.CacheAspectImpl: Retrieved from the cache

c.b.y.YoutubeHibernateApplication: Merchant is saved
org.hibernate.SQL: insert into merchant (address,email,name) values (?,?,?)
c.b.y.core.CacheRemoveAspectImpl: Cache is removed

c.b.y.YoutubeHibernateApplication: Merchant is called third time
org.hibernate.SQL: select m1_0.id,m1_0.address,m1_0.email,m1_0.name from merchant m1_0

c.b.y.YoutubeHibernateApplication: Merchant is called fourth time
org.hibernate.SQL: select m1_0.id,m1_0.address,m1_0.email,m1_0.name from merchant m1_0

As you can see, Merchant is retrieved from the database when it is called first time, when it is called second time, it is retrieved from the Cache (Redis), when a new merchant is saved, whole cached merchants are removed from the Cache (Redis).

Hibernate Cache

Hibernate is an ORM tool that allows us to interact between relational databases and Java classes. Hibernate access to the database for each request. This comes at a time-consuming cost. Hibernate’s developers built a two-level caching mechanism to prevent this time-consuming process for each request. Additionally, this two-level cache mechanism steadily upturns response time performance.

Architecture

Hibernate Caching Architecture

Hibernate Cache Level - 1

In Hibernate, the first-level cache is automatically enabled and operates within the scope of a Session object. When an entity is fetched from the database, it is cached in the Session (Persistence Context). If the same entity is requested again within the same Session, Hibernate retrieves it from the cache instead of the database. However, once the Session is closed (typically at the end of a transaction), attempting to access an entity will result in an exception, as the Session is no longer available to retrieve the entity from the cache. Hibernate manages the first-level cache automatically, so there is no need for explicit configuration.

Hibernate Cache Level 1 (memory is put there just to be understood.)

Hibernate Cache Level — 2

In concurrent processes, it’s crucial to manage how objects are shared between sessions. Hibernate’s first-level cache, specific to each session, helps maintain data integrity by ensuring that objects are not shared inappropriately between different sessions.

The second-level cache, on the other hand, is an optional feature in Hibernate that stores the state of persistent entities, making it available across sessions. When a client requests an entity, Hibernate first checks the first-level cache within the current session. If the entity is not found there, Hibernate then checks the second-level cache. If the entity is not found in either cache, Hibernate loads it from the database, caches it in both levels and returns it to the client.

In summary, while the first-level cache is essential for session-specific object management, the second-level cache provides a shared cache mechanism for entities, improving performance by reducing the need for repeated database queries in concurrent environments.

Hibernate Cache Level 2 (memory is put there just to be understood.)

implement dependencies

    implementation 'org.hibernate.orm:hibernate-jcache:6.2.7.Final'
implementation('org.ehcache:ehcache:3.10.8') {
capabilities {
requireCapability('org.ehcache:ehcache-jakarta')
}
}
implementation 'org.hibernate:hibernate-core:6.2.5.Final'

Configure application.properties or application.yaml file

spring.jpa.properties.javax.cache.provider=org.ehcache.jsr107.EhcacheCachingProvider
spring.jpa.properties.javax.cache.uri=classpath:ehcache.xml
spring.jpa.properties.hibernate.cache.use_query_cache=true
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
spring.jpa.properties.hibernate.cache.region.factory_class=jcache

The Ehcache.xml file is a configuration file used by Ehcache, a widely used open-source Java caching library. This file is used to configure various aspects of caching behaviour, such as cache settings, eviction policies and cache managers.

create a file “ecache.xml” under the “src/main/resources/” folder

<config xmlns='http://www.ehcache.org/v3'>
<cache alias="default-query-results-region">
<expiry>
<ttl unit="minutes">30</ttl>
</expiry>
<heap>1000</heap>
</cache>

<cache alias="default-update-timestamps-region">
<expiry>
<none/>
</expiry>
<heap>1000</heap>
</cache>
<cache alias="com.beratyesbek.youtubehibernate.entity.Category">
<expiry>
<ttl unit="minutes">100</ttl>
</expiry>
<heap>1000</heap>
</cache>
</config>
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.Cache;
import org.hibernate.annotations.CacheConcurrencyStrategy;


@Data
@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Cacheable
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Category {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

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

@Column(name = "code")
private String code;

}

@Cacheable annotation is not mandatory; it depends on developers.

Concurrency Strategy check Baeldung Website.

Hibernate Cache Level — 3 (Query Hint)

The term ‘Cache Level 3’ is not the correct name for this feature; it is actually called the Query cache. However, I find the name ‘Cache Level 3’ appealing.

The concept of the Query cache is straightforward: Hibernate checks if a query’s result is already cached. If the result is found in the cache, Hibernate retrieves it. If the result is not cached, Hibernate executes the query, caches the result for future use, and then returns it.

Additionally, the Query cache ensures that if the underlying data changes, any previously cached data for that query is invalidated. This ensures that clients always see the most up-to-date version of the data.

Usage of it in the Repository


import com.beratyesbek.youtubehibernate.entity.Category;
import jakarta.persistence.QueryHint;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.QueryHints;

import java.util.List;

public interface CategoryRepository extends JpaRepository<Category, Integer> {

@QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")})
List<Category> findAllByCode(String code);
}

The findAllByCode method is defined to find categories based on a given category code. The QueryHints annotation allows this method to cache results using the Hibernate query cache. This can improve performance because results can be retrieved from the cache when the same query is called again.

The @QueryHints line ensures that the results of the method’s query are cached. Cache usage is enabled by setting the value of the property named org.hibernate.cacheable to true.

Test

    @Bean
public CommandLineRunner run(CategoryRepository categoryRepository) {
return args -> {
logger.info("A category is called");
List<Category> categoriesA = categoryRepository.findAllByCode("A");

logger.info("B category is called");
List<Category> categoriesB = categoryRepository.findAllByCode("B");

logger.info("A category is called second time");
List<Category> categoriesASecondTime = categoryRepository.findAllByCode("A");

};
}

Output

org.ehcache.core.EhcacheManager: Cache 'default-update-timestamps-region' created in EhcacheManager.
org.ehcache.core.EhcacheManager: Cache 'default-query-results-region' created in EhcacheManager.
org.ehcache.core.EhcacheManager: Cache 'com.beratyesbek.youtubehibernate.entity.Category' created in EhcacheManager.

c.b.y.YoutubeHibernateApplication: A category is called
org.hibernate.SQL: select c1_0.id,c1_0.code,c1_0.name from category c1_0 where c1_0.code=?

c.b.y.YoutubeHibernateApplication: B category is called
org.hibernate.SQL: select c1_0.id,c1_0.code,c1_0.name from category c1_0 where c1_0.code=?

c.b.y.YoutubeHibernateApplication: A category is called second time

After the first call, when Category A was called for the second time, logs did not appear because of caching. Hibernate retrieved Category A from the cache, which had already been cached during the previous client request.

Advantages of Caching

Improved Performance: Caching reduces the need to fetch data from the database, which is repeatedly slower. Instead, accesses from local memory or faster data.

Reduced Latency: Caching reduces the latency by fetching the data from faster sources. (Memory, Redis, etc..)

Scalability: Caching can help improve application scalability by reducing time-response and handling more requests without overloading.

Disadvantages of Caching

Security Concerns: Caching sensitive data can pose security risks if it is not properly managed.

Stale Data: Prev versions of data, when a data is updated, cache should be updated. hard to manage (ORM has their own mechanism)

Complexity

FIRST-LEVEL CACHE vs SECOND-LEVEL CACHE

In summary

Cache plays a crucial role in enhancing application performance. Hibernate offers us a fantastic feature in this regard. In this article, I wanted to provide a perspective by showcasing my custom caching system. However, it is not recommended to use custom caching over Hibernate’s built-in caching mechanisms. Stay tuned for the next article. 😊🤟

Thanks to Furkan Sönmez for his contribution to the preparation of this article.

GitHub Source

https://github.com/BeratYesbek/youtube-hibernate/tree/hibernate-cache

--

--

Berat Yesbek
Berat Yesbek

No responses yet