SpringBoot and Hibernate – how to improve database performance

Abstract: This article is published by the Grape City technical team. Please indicate the source for reprinting: Grape City official website. Grape City provides developers with professional development tools, solutions and services to empower developers.

Foreword

In the world of software development, performance is a top priority. Whether you’re building a small web application or a large enterprise system, users expect fast and responsive software. As developers, we constantly strive to optimize our code and reduce those dreaded load times.

One of the key factors affecting application performance is database interaction. Databases are the backbone of many applications, and efficient storage and retrieval of data is critical. This is where Hibernate, the popular object-relational mapping (ORM) framework, comes into play. Hibernate simplifies the process of interacting with databases by mapping database tables to Java objects. It’s a powerful tool, but like any tool, it needs to be used wisely.

In this article, I will take you through my experience using Hibernate second-level cache to improve database performance in a Spring Boot application. We’ll delve into caching strategies, configuration settings, and best practices to improve application responsiveness. At the end of this journey, you will have the knowledge and tools to enhance database performance.

Caching

ORM frameworks like Hibernate provide a convenient way to work with databases by mapping database tables to Java objects. They cache data transparently. Caching refers to storing frequently accessed data in memory, thereby reducing the need to repeatedly query the database. This can lead to significant performance improvements, especially when working with large object graphs.

The second level cache in Hibernate is a shared cache that operates at the session factory level, making it accessible across multiple sessions. It is used to store and manage entity data so that entity data can be retrieved efficiently without repeated access to the database.

Level 2 cache description:

1. First-level cache: In Hibernate, each session (database transaction) has its own first-level cache. This cache is used to store and manage entity instances retrieved or manipulated within that session. The first level cache is isolated and tied to a specific session. When the session is closed, the first level cache is discarded.
2. Second level cache: In contrast, the second level cache is a global cache that is shared among all sessions created from the same session factory. It operates at a higher level, allowing data to be cached and retrieved across different sessions and transactions.

How the second level cache works:

When looking up an entity instance by ID (primary key) and enabling second-level cache for that entity, Hibernate performs the following steps:

Check the first level cache: Hibernate first checks the first level cache associated with the current session (session cache). If the entity instance already exists in the first-level cache, it is returned immediately to avoid database queries.
Check the second level cache: If the entity is not found in the first level cache, Hibernate will check the second level cache. The second-level cache stores entity data at a global level, making it accessible to all sessions.
Loading from the database: If the entity data is not found in the first-level cache or the second-level cache, Hibernate will continue to load the data from the database. Once the data is retrieved from the database, Hibernate assembles an entity instance and stores it in the current session’s first-level cache.
Caching for subsequent calls: Once an entity instance is in the first-level cache (session cache), all subsequent calls within the same session will return that entity instance without additional database queries. Additionally, if the entity data is fetched from the database, it can also be stored in the second-level cache for future use by other sessions.

How to use Spring Boot to use the second level cache

Step 1: Set up a Spring Boot project
You can create a new Spring Boot project using Spring Initializr or your favorite IDE. Make sure to select at least the required dependencies for Spring Boot, Spring Data JPA, Hibernate, Ehcache, and Web.

Step 2: Configure Ehcache
ehcache.xml creates the Ehcache configuration file src/main/resources in this directory. Here’s a simple example:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
    updateCheck="true" monitoring="autodetect" dynamicConfig="true">
<defaultCache
        maxEntriesLocalHeap="10000"
        eternal="false"
        timeToIdleSeconds="3600"
        timeToLiveSeconds="7200"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU" />
    
    <!-- Define specific cache regions for entities here -->
</ehcache>
  • timeToIdleSeconds: The maximum time, in seconds, that an entry can remain idle (not accessed) in the cache before being considered expired and deleted.
  • maxEntriesLocalHeap: The maximum number of cache entries stored in local heap memory. When this limit is reached, old entries are evicted to make room for new entries.
  • timeToLiveSeconds: The maximum time, in seconds, that an entry can exist in the cache before being considered expired and deleted, regardless of whether it has been accessed.
  • diskPersistent: If true, the disk storage is persistent, meaning entries will be retained even after system restarts. If false, entries will be lost on system reboot.

Step 3: Entity class
Create an entity to cache. For example, a Product entity:

@Entity
@Cacheable
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Product {<!-- -->
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private BigDecimal price;
    // Constructors, getters, and setters
}

Step 4: Repository Interface

import org.springframework.data.jpa.repository.JpaRepository;
public interface ProductRepository extends JpaRepository<Product, Long> {<!-- -->
    // Define custom query methods if needed
}

Step 5: Service Layer

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ProductService {<!-- -->
    @Autowired
    private ProductRepository productRepository;
    public Product getProductById(Long productId) {<!-- -->
        // The following query result will be cached if caching is configured properly
        return productRepository.findById(productId).orElse(null);
    }
    // Other service methods
}

Step 6: Configure application properties
Make sure application.properties you have the required properties to enable caching and specify the Ehcache configuration:

# Enable Hibernate second-level cache
spring.jpa.properties.hibernate.cache.use_second_level_cache=true
# Specify the region factory class for Ehcache
spring.jpa.properties.hibernate.cache.region.factory_class=org.hibernate.cache.ehcache.EhCacheRegionFactory
# Ehcache configuration file location
spring.cache.ehcache.config=classpath:ehcache.xml

ProductServiceHibernate and Spring Data JPA handle session and cache management when calling productRepository.findById(productId). Here’s what happens:

If the requested entity (for example) is found in the second-level cache, the entity is returned from the cache and no database session is opened.
If the entity is not found in the cache, Spring Data JPA will automatically open a database session. Execute database queries to retrieve entities.
The retrieved entities are stored in the second level cache and returned to your service method.

For more information about SpringBoot, click here.

Cache collection (one-to-many and many-to-many relationships)

Collection caching allows you to cache an entire collection of related entities. These collections can be part of a domain model, such as one-to-many or many-to-many relationships between entities.

Collection caching is valuable when dealing with associations between frequently loaded entities and where caching can significantly improve performance. When you enable collection caching, Hibernate caches the entire collection associated with an entity, such as a list or set.

When Hibernate caches a collection, it does not cache the entire collection of entities, but rather the IDs of the entities contained in the collection.

Caching just the ID can reduce memory usage compared to caching the entire entity collection.
When the collection is updated, only the relevant ID in the cache is invalidated, not the entire collection. This minimizes the overhead of cache invalidation.

@Entity
@Cacheable
public class Category {<!-- -->
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
@OneToMany(mappedBy = "category")
    @Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
    private List<Product> products;
    // Getters and setters
}
@Entity
@Cacheable
public class Product {<!-- -->
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    private String name;
    @ManyToOne
    private Category category;
    // Getters and setters
}
  • Hibernate creates a file called
    The cache area of com.example.model.Category.products. This area is dedicated to storing a cached collection of products associated with the category.
  • Let’s say we have a category with ID 1, which contains products with IDs 101, 102, and 103. Hibernate
    Store this data in the cache using key-value pairs.
  • The key-value pair might look like “Category:1:products – [101, 102, 103]”

Redis from entry to practice

One lesson will help you understand database transactions!

Chrome developer tools usage tutorial