“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.