Caching und Spring Data JPA

„Good programmers know what to write. Great ones know what to rewrite (and reuse)“

Eric S. Raymond

Im Zuge der Implementierung einer Restschnittstelle für die Verwaltung von Ahnenfoschungsvereinen kam die Frage auf, ob man die Datenbankabfragen nicht cachen könnte. Da der Bestand an solchen Vereinen sich selten verändert, liefert dieser Mikroservice fast immer identische Ergebnisse.

Bevor die notwendigen Umbauten für das Caching vorgenommen werden, gibt es einen kurzen Überblick zum Aufbau des Microservice.

Die Daten zu den Vereinen werden über die Entity Organisation persistiert. Die Annotationen @EntityListener, @CreatedDate und @LastModifiedDate dienen dazu Auditing Metadata automatisiert an der Entity zu speichern.

@Entity @EntityListeners(AuditingEntityListener.class)
@Data
public class Organisation {
  @Id @GeneratedValue @JsonIgnore
  private Long id;

  private String name;

  @CreatedDate
  private LocalDateTime createdDate;
  @LastModifiedDate
  private LocalDateTime modifiedDate;
}

Das folgende Repository für die Organisation Entities enthält eine zusätzliche generische Methode um, je nach Verwendungszweck, unterschiedliche Typen per JPA Projections abzufragen.

public interface OrganisationRepository extends CrudRepository<Organisation, Long> {
  <T> List<T> findBy(Class<T> clazz);
}

Die findBy Methode wird später entweder mit der Organisation Klasse oder der folgenden OrganisationDto Klasse aufgerufen. Die JPA Projections Implementierung benötigt neben einen speziellen Konstruktor auch eine Implementierung für equals und hashCode. Diese stellt Lombok über entsprechende Annotationen zur Verfügung. Die persistierten Organisation Instanzen besitzen alle eine eindeutige ID. Daher wird nur diese über @EqualsAndHashCode.Include für Vergleiche und den Hashcode verwendet.

@Data @AllArgsConstructor
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class OrganisationDto {
  @EqualsAndHashCode.Include @JsonIgnore
  private Long id;

  private String name;
}

Der RestController enthält zwei Endpoints mit denen, über /organisations alle und über /organisations/{id} ein spezielles Verein abgerufen werden kann.

@RestController
public class OrganisationController {
  private final OrganisationRepository repository;

  public OrganisationController(OrganisationRepository repository) {
    this.repository = repository;
  }

  @GetMapping("/organisations")
  public CollectionModel<EntityModel<OrganisationDto>> findAll() {
    List<EntityModel<OrganisationDto>> organisations = repository.findBy(OrganisationDto.class).stream()
        .map(organisation -> new EntityModel<>(organisation,
            linkTo(methodOn(OrganisationController.class).findOne(organisation.getId())).withSelfRel(),
            linkTo(methodOn(OrganisationController.class).findAll()).withRel("organisations")))
        .collect(Collectors.toList());

    return new CollectionModel<>(organisations,
        linkTo(methodOn(OrganisationController.class).findAll()).withSelfRel());
  }

  @GetMapping("/organisations/{id}")
  public EntityModel<Organisation> findOne(@PathVariable long id) {
    return repository.findById(id, Organisation.class)
        .map(organisation -> new EntityModel<>(organisation,
            linkTo(methodOn(OrganisationController.class).findOne(organisation.getId())).withSelfRel(),
            linkTo(methodOn(OrganisationController.class).findAll()).withRel("organisations"))).orElseThrow();
  }
}

Der RestController verwendet Spring HATEOAS um die Resourcen intelligent miteinander zu verknüpfen. Einzelne Resourcen werden dazu einfach in EntityModel Instanzen eingefügt und Listen von Resourcen in CollectionModel Instanzen. Diese Instanzen können mit Links zu anderen Resourcen versehen werden.

Die findAll Methode liefert Rest Respones der folgenden Art. Eine Liste von Objekten, die den Namen des Vereins, einen Link auf diese Resource und einen Link auf die Gesamtliste.

{
  "_embedded": {
    "organisationDtoList": [
      {
        "name": "Stammbaum e.V.",
        "_links": {
          "self": { "href": "http://localhost/organisations/1" },
          "organisations": { "href": "http://localhost/organisations" }
        }
      },
      {
        "name": "Ahnengalerie e.V.",
        "_links": {
          "self": { "href": "http://localhost/organisations/2" },
          "organisations": { "href": "http://localhost/organisations" }
        }
      }
    ]
  },
  "_links": {
    "self": { "href": "http://localhost/organisations" }
  }
}

Folgt man einem der Links auf eine Vereins Resource, dann wird die folgende Rest Response erzeugt. Das Objekt enthält den Namen des Vereins, die Audit Metadaten und zwei Links. Der erste Link ist ein Selbstverweis und der andere verweist auf die übergeordnete Liste. Auf diese Weise benötigt die Client-Anwendung keine speziellen Informationen zur Adressierung der Resourcen. Mit dem self-Link kann die Resource erneut geladen werden und mit dem Link unter "organisations" gelangt man zur Liste aller Vereine.

{
  "name": "Stammbaum e.V.",
  "createdDate": "2020-02-16T15:03:50.557793",
  "modifiedDate": "2020-02-16T15:03:50.557793",
  "_links": {
    "self": {
      "href": "http://localhost/organisations/1"
    },
    "organisations": {
      "href": "http://localhost/organisations"
    }
  }
}

Jeder dieser Aufrufe des RestController führt dazu, dass eine Abfrage auf der Datenbank ausgeführt wird. Da nur einige wenige Vereine in der Datenbank gespeichert sind, sollen die Datenbankzugriffe minimiert werden.

Caching wird in Spring Boot von Hause aus unterstützt. Um es zu aktivieren muss die Annotation @EnableCaching an einer der Konfigurationsklassen angefügt werden.

Danach sorgt die Annotation @Cacheable an einer Methode, dass bei passenden Argumenten, ein zuvor gespeicherter Rückgabewert verwendet wird. Existiert kein Rückgabewert für die Argumente im Cache, dann wird die Methode aufgerufen und das Ergebnis im Cache gespeichert.

Die Annotation @CacheEvict ist der Gegenspieler von @Cacheable und löscht den Cache, wenn die annotierte Methode aufgerufen wird.

Eine passende Stelle, um die Datenbank Zugriffe zu cachen, ist die OrganisationRepository Klasse. Die Implementierung der Klasse ändert sich auf folgende Version.

public interface OrganisationRepository extends CrudRepository<Organisation, Long> {
  @Cacheable("all-organisation")
  <T> List<T> findBy(Class<T> clazz);

  @Override
  @CacheEvict({"all-organisation", "one-organisation"})
  <S extends Organisation> S save(S s);

  @Override
  @Cacheable("one-organisation")
  Optional<Organisation> findById(Long var1);
}

Damit die Methoden save und findById annotiert werden können, wurden sie aus der Super-Interface CrudRepository übernommen. Die Ergebnisse der Methoden findBy und findById werden gecached und wenn die Daten eines Vereins über save geändert werden, werden die Cachewerte gelöscht.

Mit Spring Boot ist es sehr einfach, zusätzliche Funktionalitäten auf einfache und elegante Weise in bestehende Anwendungen einzubauen. Wer Caching in seiner Anwendung benötigt, kann auf Vorhandenes zurückgreifen und muss nichts neu schreiben.