REST Service mit Spring Boot und Postgres

Dass die Programmiersprache Java auch heute noch ein ernstzunehmender Kandidat für die Implementierung von Web-Applikationen ist, hängt auch mit dem Erfolg von Spring Boot zusammen. Das Framework ermöglicht es, in wenigen Codezeilen einen REST Service zu Verfügung zu stellen, der seine Daten in einer PostgreSQL Datenbank speichert.

HTTP basierte Services, die Daten im JSON Format austauschen, sind aus modernen IT Systemen nicht mehr wegzudenken. Um wirklich den Anspruch zu erfüllen, ein REST Service zu sein, ist noch etwas mehr nötig, aber auch hier ist die Basistechnologie die gleiche.

In diesem Tutorial sehen wir uns an, wie wir mit Hilfe von Spring Boot einen einfachen REST Service erstellen, der Daten in eine PostgreSQL Datenbank schreibt und wieder herausliest. Und das ganze ist weder komplex, noch dauert es lange – versprochen!

Projektstruktur REST Service mit Spring Boot

Beim Aufsetzen des Spring Boot Projekts hilft der Spring Boot Initializr: https://start.spring.io/. Nach wenigen Klicks steht uns ein fertiges Projektgerüst zum Download bereit. Das Beispielprojekt nutzt Maven als Build-Tool, mit Gradle funktioniert es aber genauso.

Wenn wir zu diesem Zeitpunkt bereits wissen, welche Abhängigkeiten wir benötigen, können wir diese direkt mit einbinden. Da wir einen REST Service erstellen wollen, der eine PostgreSQL Datenbank zur Datenspeicherung nutzt, benötigen wir in jedem Fall diese Abhängigkeiten:

  • spring-boot-starter-web: Enthält die Komponenten für die Erstellung der REST Endpunkte.
  • spring-boot-starter-data-jpa: Ermöglicht die Anbindung relationaler Datenbanken per JPA.
  • postgresql: Liefert die Treiber für die PostgreSQL Datenbank.

Weitere Spring Boot Starter oder Bibliotheken binden wir später bei Bedarf direkt in der generierten Datei pom.xml ein, die im Hauptverzeichnis des Projekts liegt.

Um diesen Artikel nicht mit zu viel Code zu überladen, habe ich ein Beispielprojekt erstellt, das Ihr gerne durchsehen oder klonen dürft. Ihr findet es unter https://gitlab.mischok-it.de/open/tasklist.

Wir starten das Projekt mit dem folgenden Kommandozeilenaufruf:

./mvnw spring-boot:run

Unter Windows funktioniert dieser Aufruf:

mvnw.cmd spring-boot:run

Danach stellt sich leider noch kein so richtiges Erfolgserlebnis ein :-( Wie auch, die Applikation findet einfach noch keine Datenbankinstanz, daher schlägt die Verbindung fehl.

PostgreSQL Datenbank starten

Zunächst kümmern wir uns um eine Datenbankinstallation, auf die wir uns verbinden können. Eine Möglichkeit ist die Nutzung eines kostenlosen Anbieters, wie zum Beispiel https://www.elephantsql.com/. Mit einem Benutzerkonto erhält man Zugriff auf eine Postgres Instanz, die zwar limitiert ist, aber für ein Beispielprojekt vollkommen ausreicht.

Alternativ können wir lokal eine Datenbank in einem Docker-Container starten. Dazu legen wir folgenden Inhalt in einer Datei docker-compose.yml an:

version: '3.5'

services:

   postgres:
      image: postgres:12
      restart: always
      environment:
         POSTGRES_USER: local
         POSTGRES_PASSWORD: local
         POSTGRES_DB: tasklist
      ports:
- 5432:5432

Führen wir nun im gleichen Ordner

docker-compose up

aus, startet der Container. Vorausgesetzt natürlich, wir haben Docker und Docker Compose auf unserem Betriebssystem installiert…

Konfiguration Postgres in Spring Boot

Ganz im gewohnten Stil von Spring Boot ist die Konfiguration des Datenbankzugriffs ein Kinderspiel: Wir fügen einfach die folgenden Zeilen in die Datei application.properties ein, die sich unter src/main/resources befindet.

spring.datasource.username=local
spring.datasource.password=local
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres
spring.datasource.driverClassName=org.postgresql.Driver

Beim aufmerksamen Lesen fällt auf, dass ich hier die Daten des oben per Docker Compose gestarteten Containers verwendet habe. Also mit Benutzername gleich Passwort… Das ist natürlich ausdrücklich nur für den lokalen Betrieb zu empfehlen! Wird eine externe Datenbankinstanz verwendet sind natürlich deren Daten einzutragen, lediglich die letzte Zeile bleibt auch in diesem Fall gleich.

Warum funktioniert das? Wir haben im Projekt per Maven die Dependency spring-boot-starter-data-jpa eingebunden. Spring Boot erkennt dies beim Hochfahren und führt „Basiskonfigurationen“ für die Datenbankverbindung durch. Der letzte Baustein, der noch fehlt, sind die Datenbank URL, die Zugangsdaten und, implizit über die Treiberklasse, die Information welche Datenbank überhaupt angebunden werden soll.

Wenn wir jetzt das Projekt starten, bekommen wir eine Erfolgsmeldung, die Verbindung hat also geklappt! Damit sind wir unserem Ziel, einen REST Service zu veröffentlichen, einen entscheidenden Schritt näher gekommen!

JPA Entitites mit Spring Data JPA

Unter der Haube greift Spring Boot, oder besser Spring Data JPA, mittels JDBC auf die Datenbank zu. Ganz allgemein könnten wir einfach gewöhnliche SQL Queries erstellen und absetzen. Als Ergebnis bekämen wir dann sogenannte Resultsets, im Endeffekt indizierte Objektlisten, aus denen wir die Spalteninhalte der gelesenen Zeilen dann herauslesen könnten.

Was würden wir damit machen? Da wir in Java unterwegs sind, wäre es naheliegend, diese Daten dann in Objekte zu schreiben und mit diesen Objekten weiterzuarbeiten. Klingt nach viel Schreibaufwand – ist es auch… Die gute Nachricht: Für genau diese Arbeit gibt es bereits Bibliotheken!

Die Jakarta Persistence API (kurz JPA) definiert das Object-Relational-Mapping für uns, also die Art und Weise, wie Datenbankinhalte in Java-Objekte überführt werden und anders herum. Bevor es zu abstrakt wird, sehen wir uns einfach ein Beispiel an.

Für das Beispielprojekt bedienen wir uns der guten alten Taskliste. Demnach enthält unsere Datenbank eine Tabelle task mit den Spalten id, title, description und done.

db structure tasklist


Um diese Struktur mittels JPA an Java Objekte anzubinden, arbeiten wir mit den Annotationen von JPA.

@Entity
@Table(name = "task")
public class Task {

   @Id
   @GeneratedValue(strategy = GenerationType.IDENTITY)
   @Column(name = "id")
   private Long id;

   @Column(name = "title")
   private String title;

   @Column(name = "description")
   private String description;

   @Column(name = "done")
   private Boolean done;
}

Java Klassen, die per JPA an eine Datenbanktabelle angebunden sind, nennen wir Entity. In der @Table Annotation auf Klassenebene wird die Verbindung zur Datenbanktabelle geschaffen. Um Datensätze in der Datenbank und Objekte auf der Java-Seite eindeutig identifizieren zu können, verwenden wir eine ID. In unserem Beispiel heißt die Spalte in der Datenbanktabelle einfach id. Mit der entsprechenden Annotation markieren wir das Feld in der Java-Klasse.

Eine Besonderheit ist die Annotation @GeneratedValue: Der Datentyp unserer ID Spalte in der Datenbank ist BIGSERIAL, PostgreSQL vergibt also automatisch die nächste „freie“ ID. Dies teilen wir JPA mit der Annotation mit, um zu verhindern, dass sich das Framework selbstständig um die Vergabe von IDs kümmern möchte.

Nicht zuletzt müssen wir natürlich noch die „einfachen“ Spalten mit den Eigenschaften in der Java Klasse verbinden. Dies macht die Annotation @Column für uns. Damit ist bereits alles an struktureller Vorarbeit geleistet, in der Theorie ist unser Quellcode bereits mit der Datenbank verbunden, im nächsten Schritt wollen wir endlich Daten lesen und schreiben!

Datenzugriff mit Spring Data JPA

Da sich die elementaren Lese- und Schreiboperationen gegen die Datenbank für alle Tabellen sehr ähnlich sind, bringt Spring Data JPA mit den sogenannten Repositories ein sehr generisches Hilfskonstrukt mit. An sich handelt es sich nur um relativ unspektakulär aussehende Interfaces – in der Praxis entfalten sie aber ihre ganze Durchschlagskraft.

Es stehen gleich mehrere Varianten zur Verfügung, ich empfehle als Allzweckwaffe eine Erweiterung des JpaRepositories:

@Repository
public interface TaskRepository extends JpaRepository<Task, Long> {}

Und wer sich fragt, was denn nun im Body des Interfaces passiert, den muss ich leider enttäuschen :-( Hier passiert tatsächlich gar nichts mehr und bei näherer Betrachtung weicht die Enttäuschung schnell einer gewissen Begeisterung.

Legen wir doch einfach einen Task an:

Task task = Task.builder()
   .description(“Don‘t forget to buy new brushes!“)
   .title(“Paint garden fence“)
   .done(false)
   .build();

task = taskRepository.save(task);

List<Task> allTasks = taskRepository.findAll();

Die Instanz taskRepository haben wir weiter oben mit @Autowired injected.

Und das war es wirklich schon…

Datenzugriff mit dem Spring Data JPA Repository

Oben habe ich das JpaRepository von Spring Data JPA als Allzweckwaffe angepriesen, spätestens jetzt wird klar, warum. Wir bekommen die Methoden zum Speichern von neuen und bestehenden Tasks, sowie diverse Methoden zum Abfragen der gespeicherten Daten aus der Postgres Datenbank schon geschenkt! Einen kompletten Überblick bietet die API Dokumentation von Spring Data JPA.

Bevor wir uns nun endlich unserem eigentlichen Thema zuwenden, also den REST Service erstellen, noch ein Wort zu den Repositories: Diese lassen sich mit einer eigenen Syntax im Handumdrehen erweitern, um auch kompliziertere Anfragen schnell umzusetzen. Ganz Spring-typisch ist aber auch dieses Feature einfach zu mächtig, um es an dieser Stelle noch ganz umreißen zu können…

Für die Implementierung unserer REST Schnittstelle reichen in der ersten Version die Standardmethoden und mit diesen machen wir uns nun auf den Weg. Bisher haben wir schon die Verbindung unserer Entities zur Datenbank hergestellt, jetzt soll die Außenwelt noch auf die Daten der Entities zugreifen können.

REST Controller in Spring Boot anlegen

In Spring Boot definiert man die HTTP Endpunkte innerhalb sogenannter Controller-Komponenten. Das Grundgerüst für unseren Task-Controller zeigt das folgende Snippet:

@Controller
@RequestMapping("tasks")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TasksController {

   private final TaskService taskService;
   ...

}

Wieder wird kräftig mit Annotationen gearbeitet, um den Code möglichst schlank zu halten. Dass es sich um einen Controller handelt signalisiert – wenig verwunderlich – die Annotation @Controller. Spannender ist die nächste Annotation: Mittels @RequestMapping("tasks") signalisieren wir, dass Aufrufe an die URL /tasks durch diesen Controller behandelt werden.

Die dritte Annotation stammt aus dem Framework Lombok, das uns im Beispielprojekt einiges an Schreibarbeit abnimmt. Darüber werden wir in der Folge noch ein paar Mal stolpern. Hier sorgt sie einfach dafür, dass alle mit final markierten Felder der Klasse automatisch bei der Erstellung der Spring Komponente injiziert werden. Wir sparen uns also die einzelne Annotation mit @Autowired.

REST Methoden mit Spring Boot erstellen

Halten wir fest: Wir haben nun einen Spring Boot Controller, der einen Task-Service injiziert bekommt. So wirklich viel können wir damit leider noch nicht anstellen… Aber es fehlt uns nur noch ein kleiner Baustein ;-)

Mit einer Klasse alleine ist in Java nicht viel anzufangen, spannend wird es, wenn wir unserer Controller-Klasse Methoden geben. Damit können wir dann wirklich HTTP Anfragen beantworten. Starten wir mit einer Methode, die alle Tasks bei einem GET Aufruf zurückgibt:

@GetMapping
public ResponseEntity<?> get() {
   return ResponseEntity.ok(
      taskService.getTasks().stream()
         .map(domainTaskToReadDtoConverter::convert)
         .collect(Collectors.toList())
      );
}

Was passiert hier? Im Wesentlichen wird eine HTTP Antwort aufgebaut, mit dem Status 200 OK. Dazu dient die Hilfsklasse ResponseEntity aus dem Spring Framework, die uns auch gleich noch die Möglichkeit gibt, einen Response Body mitzuliefern.

Hierfür verwenden wir den oben injizierten Task-Service, der ehrlich gesagt nichts anderes macht, als die Anfrage direkt an das Task-Repository weiterzugeben:

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class TaskService {
   private final TaskRepository taskRepository;

   /**
   * Reads all tasks from the store.
   * @return List of all tasks or empty list
   */
   public Collection<Task> getTasks() {
   return taskRepository.findAll();
   }

}

Spannender als die reine Datenbereitstellung ist die Frage, wie die einzelnen Elemente, die zunächst als Entities vorliegen, denn nun am besten nach außen weitergegeben werden.

JPA Entities zu DTOs konvertieren

Da JPA Entities relativ umfangreich und komplex werden können, macht es Sinn, sie nicht direkt an die HTTP Response zu übergeben, sondern sogenannte DTOs zu nutzen. DTO steht für Data Transfer Class und bezeichnet eine Klasse, die nur zur Definition einer Schnittstelle dient.

Unsere ReadTaskDto sieht so aus:

@Getter
@Builder
public class ReadTaskDto {
   private Map<String, String> links;
   private Long id;
   private String title;
   private String description;
   private boolean done;
}

Wieder eine ziemlich banale Java Klasse, der durch die Lombok Annotationen @Getter und @Builder etwas Genialität eingehaucht wird… Aber dazu gleich mehr.

Um aus einer Entity ein DTO zu machen nutzen wir keine Hilfsfunktion, sondern einen sogenannten Converter. Wir implementieren dafür das vom Spring Framework bereitgestellte Converter Interface.

@Component
public class DomainTaskToReadDtoConverter implements Converter<Task, ReadTaskDto> {

   @Override
   public ReadTaskDto convert(@NonNull Task task) {
      // Consider to extract this part...
      String selfUri = UriComponentsBuilder.fromUriString("/tasks")
         .pathSegment(String.valueOf(task.getId())).toUriString();

   Map<String, String> links = Collections.singletonMap("_self", selfUri);

   return ReadTaskDto.builder()
      .description(task.getDescription())
      .done(Optional.ofNullable(task.getDone()).orElse(false))
      .id(task.getId())
      .title(task.getTitle())
      .links(links)
      .build();
   }
}

Na also, endlich mal eine etwas komplexere Methode! Beim genaueren Hinsehen entpuppt sie sich zwar wieder als halb so spannend, aber hier passiert zumindest mal etwas…

Fangen wir von hinten an: User erwartetes Resultat, eine Instanz von ReadTaskDto, bauen wir mit einem Builder auf. Diesen schenkt uns das Framework Lombok durch die entsprechende Annotation. Hier übertragen wir mehr oder weniger eins zu eins die Felder der Entity.

Eines fällt aber noch auf: Wir erstellen ein weiteres Feld in der DTO, es heißt links und enthält – ja was denn jetzt genau? Hier kommt nun endlich REST ins Spiel: Unsere Schnittstelle generiert in jedem Fall den sogenannten Self-Link, also die Adresse, unter der ein Element als Einzelressource abgerufen werden kann. Unter diesem Link kann es auch editiert oder gelöscht werden. Für den Moment nehmen wir das einfach mal so hin...

Spring Boot REST Service aufrufen

Auch wenn eine gewisse Enttäuschung vorprogrammiert ist, starten wir zu diesem Zeitpunkt unser Projekt einfach mal. Dazu geben wir wieder auf einem beliebigen Terminal das folgende Kommando ein:

./mvnw spring-boot:run

Bzw. unter Windows wieder:

mvnw.cmd spring-boot:run

Damit sollte unsere Applikation jetzt ohne Probleme hochfahren. Dass alles erfolgreich war, sehen wir an dieser Meldung im Konsolenoutput:

2021-10-18 14:59:21.451 INFO 67275 --- [ main] d.m.a.tasklist.TasklistApplication : Started TasklistApplication in 2.478 seconds (JVM running for 2.744)

Damit sind wir schon fast am Ziel: Wenn wir nun in einem Browser unserer Wahl die URL http://localhost:8080/tasks aufrufen, sehen wir schon einen gültigen Output.

localhost.png

Zwar noch eine leere Liste, aber zumindest keine Fehlermeldung.

URL Mapping auf Spring Boot Controller

Bevor wir gleich sinnvolle Daten anlegen, noch ein Blick welchen Weg unsere Daten nun von der PostgreSQL Datenbank bis zum Browser nehmen.

Im Browser wird mit der HTTP Methode GET die URL http://localhost:8080/tasks aufgerufen. Die Spring Boot Applikation lauscht auf localhost:8080 und entscheidet dann, welcher Controller für den GET auf /tasks zuständig ist. Dazu wird zuerst die @RequestMapping Annotation der Controller gescanned. Hier findet der Match für den TaskController statt. Innerhalb des Controllers wird nun die Java-Methode gesucht, die das Mapping für den HTTP-Aufruf mit der Methode GET hat. Dies wird mit @GetMapping angegeben.

Naja, und dann passiert das zuvor oben bereits besprochene ;-) Über den TaskService wird das Repository aufgerufen und die Entities zu DTOs gepackt und zurückgegeben. Nachdem unsere Datenbanktabelle aber noch leer ist, gibt es natürlich nur eine leere Liste zu bestaunen, in JSON eben ein leeres Array: [].

Mit Spring REST Service Daten in Postgres schreiben

Um neue Task Einträge in die PostgreSQL Datenbank zu schreiben, nutzen wir den ebenfalls im TaskController definierten Endpunkt POST /tasks. Hiermit fügen wir eine weitere REST Ressource hinzu. Auf meinem Linux Gerät mache ich das am liebsten per Kommandozeile mittels curl. Natürlich funktioniert es auch mit einem graphischen Tool wie Postman.

curl -X POST localhost:8080/tasks -H "Content-Type: application/json" -d '{"title": "Get started with Spring Boot", "description": "Check out H2 as in-memory solution!"}'

Unsere Payload, also die zu schreibenden Daten übergeben wir als JSON-String, außerdem müssen wir beim Aufruf noch den Content-Type festlegen. Um zu prüfen, ob es geklappt hat, aktualisieren wir einfach unseren zuvor geöffnetes Browserfenster und siehe da:

json

Unser eben angelegter Task erscheint in der Liste! Verantwortlich dafür ist die folgende Methode:

@PostMapping
public ResponseEntity<?> post(@RequestBody @Valid SaveTaskDto dto) {
   Task task = dtoToDomainConverter.convert(dto);
   task = taskService.create(task);
   URI uri = UriComponentsBuilder.fromUriString
   ("/tasks").pathSegment(String.valueOf(task.getId())). build().toUri();
   return ResponseEntity.created(uri).build();
}

Die Methode erhält eine Instanz einer DTO, hier werden die zu speichernden Daten hinterlegt. Bis zu Datenbank wird jetzt einfach der entgegengesetzte Weg wie beim Lesen gegangen: Aus der DTO wird per Converter eine Entity gemacht, diese speichert der TaskService mit Hilfe des Repositorys in die Postgres Datenbank:

public Task create(@NonNull Task task) {
   return taskRepository.save(task);
}

Dieser Artikel ist leider jetzt schon viel zu umfangreich, um noch im Detail auf weitere Methoden einzugehen: Daten aktualisieren, Einzeldatensätze abfragen, löschen, etc… Das Beispielsprojekt unter https://gitlab.mischok-it.de/open/tasklist ist aber komplett implementiert, es lohnt sich bei Interesse in jedem Fall einen Blick darauf zu werfen!

Ist ein Spring Controller schon RESTful?

An dieser Stelle werden wir wieder philosophisch: Haben wir hiermit bereits eine RESTful API erstellt? Ist jeder Spring Controller schon RESTful?

Die generelle Antwort lautet sicherlich „Nein“. Mit einem beliebigen Spring Controller kann man auch Schnittstellen schreiben, die nicht RESTful sind. Unser Beispielprojekt erfüllt aber einige Vorgaben, die an REST Schnittstellen gestellt werden: Sie ist zustandslos, jede Ressource (bei uns sind das die Tasks) erhält eine eigene eindeutige Adresse, und noch viel mehr.

In einem späteren Post werde ich noch einmal genauer darauf eingehen, wie eine einfache REST API strukturiert sein sollte um wirklich RESTful zu sein – wie man sie dann mit Spring Boot und PostgreSQL umsetzt wissen wir ja bereits!

Spring-Boot News

Bleibe auf dem aktuellsten Stand mit unseren kostenlosen Spring-Boot updates. So wirst Du direkt informiert, wenn wir einen neuen Artikel veröffentlichen.
Kein Spam, kein Bullshit, nur Spring-Boot Insider-Wissen, versprochen.

Noch keine Kommentare vorhanden

Was denkst du?

© 2020 Mischok