Come Integrare NCache con JPA Hibernate per il Caching in Applicazioni Spring Boot

Che cos’è JPA Hibernate?

Hibernate è uno dei più popolari Object Relational Mapper (ORM) per applicazioni Java e Spring. Aiuta gli sviluppatori a connettersi e lavorare con database relazionali da applicazioni Java senza dover scrivere query SQL. La libreria implementa la specifica JPA (Java Persistence API) e offre diverse funzionalità aggiuntive che facilitano lo sviluppo della persistenza nelle applicazioni in modo più veloce ed efficiente.

Caching in JPA Hibernate

Una delle caratteristiche interessanti supportate da Hibernate è il caching. Hibernate supporta due livelli di caching — L1 e L2. Il cache L1 è abilitato per impostazione predefinita e funziona all’interno dell’ambito di un’applicazione, quindi non può essere condiviso tra più thread. Ad esempio, se si dispone di un’applicazione di microservizi scalata che legge e scrive in una tabella in un sistema di database relazionale, questo cache L1 viene mantenuto individualmente in ciascuno di questi contenitori in cui è in esecuzione il microservizio.

Il cache L2 è un’interfaccia esterna e pluggable, grazie alla quale possiamo memorizzare nella cache i dati accessibili frequentemente in un provider di caching esterno tramite Hibernate. In questo caso, il cache viene mantenuto al di fuori della sessione e può essere condiviso attraverso lo stack di microservizi (nell’esempio sopra).

Hibernate supporta il cache L2 con la maggior parte dei provider di caching popolari come Redis, Ignite, NCache, ecc.

Che cos’è NCache?

NCache è uno dei provider di caching distribuito più popolari disponibili nel mercato. Offre diverse funzionalità e supporta l’integrazione con stack di programmazione popolari come .NET, Java, ecc.

NCache è disponibile in diverse varianti — open source, professional ed enterprise — e puoi scegliere tra queste in base alle funzionalità che offrono.

Integrare NCache con Hibernate

NCache supporta l’integrazione con Hibernate come L2 Cache e anche per il caching delle query. Utilizzando un cluster di cache distribuita esterna, possiamo assicurarci che le entità accessibili frequentemente siano memorizzate nella cache e utilizzate attraverso i microservizi in un ambiente scalato senza imporre un carico indesiderato sul livello di database. In questo modo, le chiamate al database sono mantenute al minimo possibile e le prestazioni dell’applicazione sono ottimizzate.

Per iniziare, aggiungiamo i pacchetti necessari al nostro progetto spring boot. Per dimostrare, sto utilizzando un JPA Repository che usa Hibernate ORM per interagire con il database relazionale — configurazione MySQL.

Le dipendenze nel mio file pom.xml sono così:

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>

Il mio JPARepository legge e scrive in una tabella chiamata books nel mio database MySQL. Il repository e l’entità sono simili al seguente:

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);

    // salva l'entità

    repository.save(book);

    // conferma le modifiche

    repository.flush();

    // restituisci l'id generato

    var bookId = book.getBookId();

    return bookId;
  }

  public Book findBook(int id) {
    var entity = repository.findById(id);

    if (entity.isPresent()) {
      return entity.get();
    }

    return null;
  }
}

Mentre questa configurazione funziona perfettamente, non abbiamo aggiunto alcun caching. Vediamo come possiamo integrare il caching con Hibernate utilizzando NCache come provider.

Caching di livello 2 con NCache

Per integrare NCache con Hibernate, aggiungeremo altre due dipendenze al nostro progetto. Queste sono mostrate di seguito:

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>

Aggiungeremo anche un file Hibernate.cfg.xml dove configureremo il cache di secondo livello e i dettagli sono indicati di seguito:

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>

In cima all’entità Libro, aggiungeremo un’annotazione che imposterà lo stato cache per l’entità:

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.

Il file client.nconf appare come di seguito:

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>

Quando eseguo il mio applicativo con questa configurazione e faccio un’operazione GET per un singolo libro, Hibernate cerca l’entità nel cluster NCache e restituisce l’entità memorizzata nella cache; se non è presente, mostra un cache miss.

Caching delle query con NCache

Un’altra caratteristica di Hibernate che NCache supporta completamente è il caching delle query. In questo approccio, il set di risultati di una query può essere memorizzato nella cache per dati accessibili frequentemente. Ciò garantisce che il database non sia interrogato frequentemente per dati accessibili frequentemente. Questo è specifico per le query HQL (Hibernate Query Language).

Per abilitare il caching delle query, aggiungerò semplicemente un’altra riga nel file Hibernate.cfg.xml:

XML

 

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

Nel repository, creerò un altro metodo che eseguirà una query specifica, e il risultato verrà memorizzato nella cache.

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 questo metodo, sto eseguendo una query su tutti i libri che iniziano con la lettera T, e l’insieme dei risultati deve essere memorizzato nella cache. Per fare ciò, aggiungerò un suggerimento di query che imposterà il caching su true.

Quando interagiamo con l’API che chiama questo metodo, possiamo vedere che l’intero set di dati è ora memorizzato nella cache.

Conclusione

Il caching è una delle strategie più utilizzate nella costruzione di applicazioni distribuite. In un’architettura a microservizi, dove un’applicazione viene scalata X volte in base alla domanda, colpire spesso il database per i dati può essere dispendioso.

I provider di caching come NCache offrono una soluzione facile e incappucciata per i microservizi Java che utilizzano Hibernate per interrogare i database. In questo articolo, abbiamo visto come utilizzare NCache come Livello 2 Cache per Hibernate e come utilizzarlo per memorizzare nella cache entità individuali e caching delle query.

Source:
https://dzone.com/articles/how-to-integrate-ncache-with-jpa-hibernate-for-cac