Loading...

Vermeidung mehrfacher Datenabrufe durch den First-Level-Cache in Spring Data JPA

JPA
12. Juli 2023
4 Minuten Lesezeit
Beitrag teilen:
Gefällt dir der Beitrag?
Du wirst den Newsletter lieben!

Wenn du dich mit der Backendentwicklung auf der JVM befasst, wirst du sicherlich auf die Java Persistence API (JPA) stoßen. Ein bekanntes Implementierungsframework dafür ist Hibernate. In diesem Artikel zeigen wir dir, wie du in Spring Data JPA mit caching die Performance deiner Anwendung optimieren kannst, indem du verhinderst, dass gleiche Ressourcen mehrfach abgerufen werden.

Was ist der First-Level-Cache?

Spring Data JPA verwendet Hibernate als Standard-ORM (Object-Relational Mapping), das einen eingebauten First-Level-Cache anbietet. Jede Session hat ihren eigenen Cache, den wir als First-Level-Cache bezeichnen. Jedes Mal, wenn du ein Objekt (oder genauer gesagt eine Entity-Instanz) aus der Datenbank abrufst, wird es zuerst im First-Level-Cache gespeichert. Bei wiederholtem Abrufen desselben Objekts wird es direkt aus dem Cache und nicht aus der Datenbank geholt. Das kann zur Datenwiederholung führen und die Performance beeinträchtigen.

Wie @Transactional den Unterschied macht

Der First-Level-Cache ist immer mit der aktuellen Hibernate-Session verknüpft, die wiederum an eine laufende Transaktion gebunden ist. Der Einsatz der Annotation @Transactional ist entscheidend. Jede Methode, die mit @Transactional annotiert ist, wird in einer separaten Transaktion ausgeführt, und jede Transaktion hat ihren eigenen First-Level-Cache.

@Service
public class MyService {
    private final MyRepository repository;

    public MyService(MyRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public void demonstrateFirstLevelCache(Long id) {
        MyEntity entity1 = repository.findById(id).orElseThrow();
        // Bearbeitungen auf entity1...
        // ...

        // Erneuter Aufruf von findById für die gleiche ID
        MyEntity entity2 = repository.findById(id).orElseThrow();
        // Da das Objekt bereits im First-Level-Cache liegt, wird es nicht aus der Datenbank abgerufen.
        // Also, entity1 und entity2 sind eigentlich dasselbe Objekt (== wäre true)

        // Bearbeitungen auf entity2...
        // ...
    }
}

In diesem Code-Beispiel hat die Methode demonstrateFirstLevelCache ihre eigene Transaktion und damit ihren eigenen First-Level-Cache. Obwohl wir die Methode mehrmals mit der gleichen ID aufrufen, wird die Entität wird nur einmal aus der Datenbank abgerufen. Beim zweiten Aufruf wird die Entität aus dem Cache geholt.

Wie man das wiederholte Abrufen derselben Ressource verhindert

Manchmal können wir, ohne es zu merken, in einem Anwendungsfall mehrfach die gleiche Ressource abrufen. Dies kann passieren, wenn wir mehrere Transaktionen in einem einzigen Codepfad haben. Jede Transaktion hat ihren eigenen First-Level-Cache, und daher wird die gleiche Entität für jede Transaktion aus der Datenbank geholt. Schauen wir uns das folgende Beispiel an:

@Service
public class MyService {
    private final MyRepository repository;

    public MyService(MyRepository repository) {
        this.repository = repository;
    }

    @Transactional
    public MyEntity retrieveEntity(Long id) {
        return repository.findById(id).orElseThrow();
    }

    public void processEntityTwice(Long id) {
        MyEntity entity1 = retrieveEntity(id);
        // Bearbeitungen auf entity1...
        // ...
        
        // Eine zweite Transaktion wird gestartet
        MyEntity entity2 = retrieveEntity(id);
        // Bearbeitungen auf entity2...
        // ...
    }
}

In diesem Beispiel wird retrieveEntity zweimal aufgerufen, wobei jeder Aufruf eine eigene Transaktion startet. Daher wird die gleiche Entität für jede Transaktion aus der Datenbank geholt.

Eine effizientere Möglichkeit besteht darin, alle benötigten Daten in einer einzigen Transaktion zu holen. Hier ist ein verbessertes Beispiel:

@Service
public class MyService {
    private final MyRepository repository;

    public MyService(MyRepository repository) {
        this.repository = repository;
    }
    
    @Transactional
    public void processEntityOnce(Long id) {
        MyEntity entity1 = repository.findById(id).orElseThrow();
        // Bearbeitungen auf entity1...
        // ...
        
        // Erneuter Aufruf von findById innerhalb der gleichen Transaktion
        MyEntity entity2 = repository.findById(id).orElseThrow();
        // Da das Objekt bereits im First-Level-Cache liegt, wird es nicht aus der Datenbank abgerufen.
        // Also, entity1 und entity2 sind eigentlich dasselbe Objekt (== wäre true)
        
        // Bearbeitungen auf entity2...
        // ...
    }
}

In diesem Codebeispiel holt die processEntityOnce Methode die gleiche Entität nur einmal aus der Datenbank und speichert sie im First-Level-Cache. Bei einem erneuten Abruf innerhalb derselben Transaktion holt Hibernate das Objekt direkt aus dem First-Level-Cache und nicht aus der Datenbank. Dadurch wird die Datenbankbelastung reduziert und die Anwendungsperformance verbessert.

Zusammenfassung

In der Backendentwicklung mit Spring Data JPA und Hibernate ist es wichtig zu verstehen, wie der First-Level-Cache funktioniert und wie man ihn am besten einsetzt, um die Effizienz und Leistungsfähigkeit der Anwendung zu optimieren. Mit dem richtigen Einsatz von @Transactional und einem bewussten Umgang mit Datenabrufen kann man eine signifikante Verbesserung der Anwendungsperformance erzielen.

Vergiss nicht, dass es immer noch viele Aspekte in Hibernate und Spring JPA zu entdecken gibt, und jedes Projekt kann einzigartige Herausforderungen bieten. Aber mit dem soliden Verständnis des Caching-Mechanismus und der Transaktionsverwaltung, das du jetzt hast, bist du gut gerüstet, um sie zu meistern!

Kennst du schon Marcus' Backend Newsletter?

Neue Ideen. 2x pro Woche!
Top