How To Integrate NCache With JPA Hibernate for Caching in Spring Boot Applications

In this article, let’s discuss the caching feature of Hibernate with NCache for caching in a Spring Boot application with an illustrating example.

What Is JPA Hibernate?

Hibernate is one of the most popular Object Relational Mapper (ORM) libraries for Java and Spring applications. It helps developers connect to and work with relational databases from Java applications without having to write SQL queries. The library implements the JPA (Java Persistence API) specification and provides several additional features that help develop persistence in applications faster and easier.

Caching in JPA Hibernate

One of the cool features supported by Hibernate is caching. Hibernate supports two levels of caching — L1 and L2. L1 cache is enabled by default and works within an application scope, so it cannot be shared across multiple threads. For example, if you have a scaled microservice application that reads and writes to a table in a relational database setup, this L1 cache is maintained individually within each of these containers where the microservice is running.

L2 cache is an external pluggable interface, using which we can cache frequently accessed data in an external caching provider via Hibernate. In this case, the cache is maintained out of session and can be shared across the microservice stack (in the above example).

Hibernate supports L2 cache with most of the popular caching providers like Redis, Ignite, NCache, etc.

What Is NCache?

NCache is one of the most popular distributed caching providers available in the market. It offers several features and supports integration with popular programming stacks like .NET, Java, etc.

NCache comes in several flavors — open source, professional, and enterprise and you can choose from these based on the features they offer.

Integrating NCache With Hibernate

NCache supports integration with Hibernate as L2 Cache and also for query caching. By using an external distributed cache cluster, we can ensure that frequently accessed entities are cached and used across the microservices in a scaled environment without putting unwanted load on the database layer. This way, the database calls are kept as minimal as possible, and the application performance is optimized as well.

To get started, let’s add the necessary packages to our spring boot project. To demonstrate, I’m going with a JPA Repository that uses Hibernate ORM to work with the relational database — MySQL setup.

Dependencies in my pom.xml file looks like this:

XML
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> </dependencies>

My JPARepository reads and writes to a table called books in my MySQL database. The repository and the entity looks like the following:

Java
package com.myjpa.helloapp.repositories; import com.myjpa.helloapp.models.entities.Book; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface BookRepository extends JpaRepository<Book, Integer> {}
Java
package com.myjpa.helloapp.models.entities;
import jakarta.persistence.*;
import java.util.Date;
import org.hibernate.annotations.CreationTimestamp;
@Entity(name = "Book")
@Table(name = "Book")
public class Book {
  @Id @GeneratedValue(strategy = GenerationType.AUTO) private int bookId;
  @Column(name = "book_name") private String bookName;
  @Column(name = "isbn") private String isbn;
  @CreationTimestamp @Column(name = "created_date") private Date createdDate;
  public Book() {}
  public Book(String bookName, String isbn) {
    this.bookName = bookName;
    this.isbn = isbn;
  }
  public int getBookId() {
    return bookId;
  }
  public String getBookName() {
    return bookName;
  }
  public String getIsbn() {
    return isbn;
  }
  public Date getCreatedDate() {
    return createdDate;
  }
}

A BookService interacts with this repository and exposes GET and INSERT functionalities.

Java
package com.myjpa.helloapp.services;
import com.myjpa.helloapp.models.entities.Book;
import com.myjpa.helloapp.repositories.BookRepository;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookService {
  @Autowired
  private BookRepository repository;
  public int createNew(String bookName, String isbn) {
    var book = new Book(bookName, isbn);
    // save the entity
    repository.save(book);
    // commit the changes
    repository.flush();
    // return the generated id
    var bookId = book.getBookId();
    return bookId;
  }
  public Book findBook(int id) {
    var entity = repository.findById(id);
    if (entity.isPresent()) {
      return entity.get();
    }
    return null;
  }
}

While this setup works perfectly fine, we haven’t added any caching to this. Let’s see how we can integrate caching to Hibernate with NCache as the provider.

L2 Caching With NCache

To Integrate NCache with Hibernate, we will add two more dependencies to our project. These are shown below:

XML
<dependency> <groupId>com.alachisoft.ncache</groupId> <artifactId>ncache-hibernate</artifactId> <version>5.3.2</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-jcache</artifactId> <version>6.4.2.Final</version> </dependency>

We will also add a Hibernate.cfg.xml file where we will configure the second-level cache and details below:

XML
<hibernate-configuration> <session-factory> <property name="hibernate.cache.use_second_level_cache">true</property> <property name="hibernate.cache.region.factory_class">JCacheRegionFactory</property> <property name="hibernate.javax.cache.provider" >com.alachisoft.ncache.hibernate.jcache.HibernateNCacheCachingProvider</property> <property name="ncache.application_id">booksapi</property> </session-factory> </hibernate-configuration>

On top of the Entity Book, we will add an annotation that will set the cache status for the entity:

Java
@Entity(name = "Book")
@Table(name = "Book")
@Cache(region = "demoCache", usage = CacheConcurrencyStrategy.READ_WRITE)
public class Book {}

I’m indicating that my entities will be cached under the region demoCache, which is basically my cache cluster name.

I’d also place my client.nconf and config.nconf files, which contain information about the cache cluster and its network details in the root directory of my project.

The client.nconf looks like below:

XML
<?xml version="1.0" encoding="UTF-8"?> <!-- Client configuration file is used by client to connect to out-proc caches. Light weight client also uses this configuration file to connect to the remote caches. This file is automatically generated each time a new cache/cluster is created or cache/cluster configuration settings are applied. --> <configuration> <ncache-server connection-retries="5" retry-connection-delay="0" retry-interval="1" command-retries="3" command-retry-interval="0.1" client-request-timeout="90" connection-timeout="5" port="9800" local-server-ip="192.168.0.108" enable-keep-alive="False" keep-alive-interval="0" /> <cache id="demoCache" client-cache-id="" client-cache-syncmode="optimistic" skip-client-cache-if-unavailable="False" reconnect-client-cache-interval="10" default-readthru-provider="" default-writethru-provider="" load-balance="True" enable-client-logs="True" log-level="info"> <server name="192.168.0.108" /> </cache> </configuration>

When I run my application with this setup and do a GET operation for a single book, Hibernate looks up the entity in the NCache cluster and returns the cache entity; if it is not present, it shows a cache miss.

a GET operation

the NCache cluster

cache entity

Query Caching With NCache

Another feature of Hibernate that NCache fully supports is query caching. In this approach, the result set of a query can be cached for frequently accessed data. This ensures that the database is not frequently queried for frequently accessed data. This is specific for HQL (Hibernate Query Language) queries.

To enable Query Caching, I’ll simply add another line to the Hibernate.cfg.xml below:

XML
<property name="hibernate.cache.use_query_cache">true</property>

In the repository, I’d create another method that will run a specific query, and the result is cached.

Java
@Repository public interface BookRepository extends JpaRepository<Book, Integer> { @Query(value = "SELECT p FROM Book p WHERE bookName like 'T%'") @Cacheable(value = "demoCache") @Cache(usage = CacheConcurrencyStrategy.READ_ONLY, region = "demoCache") @QueryHints(value = { @QueryHint(name = "org.hibernate.cacheable", value = "true") }) public List<Book> findAllBooks(); }

In this method, I’m querying for all Books that start with the letter T, and the result set is to be cached. For this, I’ll add a query hint that will set the caching to true.

When we hit the API that calls this method, we can see that the entire dataset is now cached.

the entire dataset is now cached

Conclusion

Caching is one of the most used strategies in building distributed applications. In a microservice architecture, where one application is scaled to X times based on the load, frequently hitting a database for data can be costly.

Caching providers like NCache provide an easy and pluggable solution to Java microservices that use Hibernate for querying databases. In this article, we have seen how to use NCache as an L2 Cache for Hibernate and use it to cache individual entities and query caching.

You may also like