So integrieren Sie NCache mit JPA Hibernate für Caching in Spring Boot-Anwendungen

Was ist JPA Hibernate?

Hibernate ist einer der beliebtesten Object Relational Mapper (ORM) Bibliotheken für Java- und Spring-Anwendungen. Es hilft Entwicklern, Verbindungen zu relationalen Datenbanken aus Java-Anwendungen herzustellen und damit zu arbeiten, ohne SQL-Abfragen schreiben zu müssen. Die Bibliothek implementiert die JPA (Java Persistence API) Spezifikation und bietet zusätzliche Funktionen, die die Entwicklung von Persistenz in Anwendungen schneller und einfacher machen.

Caching in JPA Hibernate

Eine der coolen Funktionen, die Hibernate unterstützt, ist Caching. Hibernate unterstützt zwei Ebenen des Cachings — L1 und L2. Der L1 Cache ist standardmäßig aktiviert und arbeitet im Anwendungsumfang, kann also nicht über mehrere Threads hinweg geteilt werden. Zum Beispiel, wenn Sie eine skalierte Microservice-Anwendung haben, die Lese- und Schreibvorgänge auf einer Tabelle in einer relationalen Datenbank-Setup ausführt, wird dieser L1 Cache individuell in jedem dieser Container beibehalten, in denen der Microservice ausgeführt wird.

Der L2 Cache ist eine externe, pluggare Schnittstelle, über die wir häufig zugegriffene Daten über Hibernate in einem externen Caching-Anbieter zwischenspeichern können. In diesem Fall wird der Cache außerhalb der Sitzung beibehalten und kann im obigen Beispiel über die Microservice-Stapel geteilt werden.

Hibernate unterstützt den L2 Cache mit den meisten beliebten Caching-Anbietern wie Redis, Ignite, NCache usw.

Was ist NCache?

NCache ist einer der beliebtesten verteilten Zwischenspeicheranbieter auf dem Markt. Es bietet verschiedene Funktionen und unterstützt die Integration in beliebte Programmierstacks wie .NET, Java usw.

NCache steht in verschiedenen Varianten – Open Source, Professional und Enterprise – und Sie können je nach den angebotenen Funktionen wählen.

Integration von NCache mit Hibernate

NCache unterstützt die Integration mit Hibernate als L2-Cache und auch für Abfrage-Caching. Indem wir einen externen verteilten Cache-Cluster verwenden, können wir sicherstellen, dass häufig zugegriffene Entitäten zwischengespeichert und in einer skalierten Umgebung über Microservices verwendet werden, ohne unnötigen Druck auf die Datenbankebene auszuüben. Auf diese Weise werden Datenbankanrufe so minimal wie möglich gehalten und die Anwendungsleistung wird ebenfalls optimiert.

Um loszulegen, fügen wir zunächst die notwendigen Pakete zu unserem Spring Boot-Projekt hinzu. Um zu demonstrieren, verwende ich ein JPA-Repository, das Hibernate ORM verwendet, um mit der relationalen Datenbank – MySQL-Setup – zu arbeiten.

Die Abhängigkeiten in meiner pom.xml-Datei sehen folgendermaßen aus:

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>

Mein JPARepository liest und schreibt in eine Tabelle namens Bücher in meiner MySQL-Datenbank. Das Repository und die Entität sehen folgendermaßen aus:

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

    // speichere die Entität 

    repository.save(book);

    // bestätige die Änderungen 

    repository.flush();

    // gib die generierte ID zurück

    var bookId = book.getBookId();

    return bookId;
  }

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

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

    return null;
  }
}

Während diese Einstellung einwandfrei funktioniert, haben wir noch keine Caching hinzugefügt. Schauen wir uns an, wie wir Caching mit Hibernate und NCache als Anbieter integrieren können.

L2-Caching mit NCache

Um NCache mit Hibernate zu integrieren, werden wir zwei weitere Abhängigkeiten zu unserem Projekt hinzufügen. Diese sind unten dargestellt:

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>

Wir werden auch eine Hibernate.cfg.xml-Datei hinzufügen, in der wir das Zwischencache-Setup und die Details unten konfigurieren:

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>

Oberhalb der Entity Buch fügen wir eine Anmerkung hinzu, die den Cache-Status für die Entity festlegt:

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.

Die client.nconf sieht wie folgt aus:

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>

Wenn ich mein Anwendung mit dieser Einstellung ausführe und eine GET-Operation für ein einzelnes Buch durchführe, sucht Hibernate die Entity im NCache-Cluster und gibt die gespeicherte Entity zurück; falls sie nicht vorhanden ist, ergibt sich ein Cache-Miss.

Abfrage-Caching mit NCache

Eine weitere Funktion von Hibernate, die NCache voll unterstützt, ist Abfrage-Caching. Bei diesem Ansatz kann das Ergebnis einer Abfrage für häufig zugreifbare Daten zwischengespeichert werden. Dies stellt sicher, dass die Datenbank nicht häufig für häufig zugreifbare Daten abgefragt wird. Dies ist spezifisch für HQL (Hibernate Query Language)-Abfragen.

Um die Abfragezustellung zu aktivieren, füge ich einfach eine weitere Zeile in das Hibernate.cfg.xml unter

XML

 

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

ein. Im Repository würde ich eine weitere Methode erstellen, die eine spezifische Abfrage ausführt, und das Ergebnis wird zwischengespeichert.

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 dieser Methode frage ich nach allen Büchern, die mit dem Buchstaben T beginnen, und die Ergebnismenge soll zwischengespeichert werden. Dafür füge ich einen Abfragehinweis hinzu, der die Zwischenspeicherung auf „true“ setzt.

Wenn wir die API aufrufen, die diese Methode aufruft, können wir sehen, dass die gesamte Datenmenge jetzt zwischengespeichert ist.

Schlussfolgerung

Zwischenspeicherung ist eine der am häufigsten eingesetzten Strategien bei der Erstellung verteilter Anwendungen. In einer Microservice-Architektur, bei der eine Anwendung auf X-fach skaliert wird, abhängig von der Belastung, kann das häufige Abfragen von Daten aus einer Datenbank kostspielig sein.

Zwischenspeicherungsprovider wie NCache bieten eine einfache und einfach einsetzbare Lösung für Java-Microservices, die Hibernate zur Abfrage von Datenbanken verwenden. In diesem Artikel haben wir gesehen, wie man NCache als L2 Cache für Hibernate verwendet und es zur Zwischenspeicherung einzelner Entitäten und zur Abfragezustellung nutzt.

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