Die neue JdbcClient-Klasse im Spring Framework 6.1

Die neue JdbcClient-Klasse im Spring Framework 6.1

Steht eine Ablösung von JDBCTemplate bevor?

Mit Einführung des Spring Framework 6.1 das Teil von Spring Boot 3.2 ist wurde eine neue Klasse JdbcClient eingeführt, welche ein Wrapper um die vorhandene Klasse JdbcTemplate ist um Datenbankoperationen mit einer Fluent-API durchzuführen.

Mit Veröffentlichung von Spring Boot 3.2 steht uns diese Klasse zu Verfügung. Wir wollen einen Blick darauf werfen wie wir damit unterschiedliche Datenbankoperationen ausführen.

Zunächst erstellen wir eine neue Applikation unter https://start.spring.io. Dabei wählen wir Spring JDBC, Postgresql Driver, Flyway Migration und Testcontainers aus. Wir müssen zum Test kein PostgreSQL installieren. Durch die Verwendung von Testcontainer wird PostgreSQL automatisch beim Test in einem separaten Container gestartet.

Zunächst erstellen wir eine Domänenklasse

Starten wir mit einer Record-Klasse die Kontakte enthält

package de.saphirgmbh.jdbcexample; import java.time.Instant; public record Kontakte(Long id, String bezeichnung, String email, String telefon, Instant erstelltAm) { }

Flyway-Skript erstellen

Damit auch eine Datenbanktabelle angelegt wird verwenden wir Flyway, um diese anzulegen

Dazu erstellen wir die Skriptdatei im Ordner src/main/resources/db/migration

create table kontakte ( id bigserial primary key, bezeichnung varchar not null, email varchar, telefon varchar, erstellt_am timestamp );

Implementierung der CRUD-Operationen mit der Klasse JdbcClient

Nun können wir mit der Implementierung der CRUD-Operationen mithilfe der Klasse JdbcClient beginnen.

@Repository @Transactional(readOnly = true) public class KontaktRepository { private final JdbcClient jdbcClient; public KontaktRepository(JdbcClient jdbcClient) { this.jdbcClient = jdbcClient; } ... ...

Alle Kontakte lesen

Alle Kontakte können mit dem 'JdbcClient' folgendermaßen gelesen werden:

private final static String SELECT_ALL = """ SELECT id, bezeichnung, email, telefon, erstellt_am FROM kontakte """; public List<Kontakt> findAll() { return jdbcClient.sql(SELECT_ALL).query(Kontakt.class).list(); }

Die Klasse JdbcClient erzeugt in diesem Fall dynamisch einen RowMapper vom Typ SimplePropertyTypeRowMapper. Dieser erledigt das Mapping der Datenbankspalten in die Attribute der Java Klasse. Dabei wird camelCase in Unterstrich umgewandelt.

Sollte das auf Grund eines vorhandenen Schemas nicht passen kann ein eigener Mapper implementiert werden

public List<Kontakt> findAll() { return jdbcClient.sql(SELECT_ALL).query(new KontaktRowMapper()).list(); } static class KontaktRowMapper implements RowMapper<Kontakt> { @Override public Kontakt mapRow(ResultSet rs, int rowNum) throws SQLException { return new Kontakt(rs.getLong("id") , rs.getString("bezeichnung") , rs.getString("email") , rs.getString("telefon") , rs.getTimestamp("erstellt_am").toInstant()); } }

Suchen eines Kontaktes über dessen ID

Ein Kontakt kann auch über die ID gelesen werden

public Optional<Kontakt> findById(Long id) { return jdbcClient.sql(SELECT_ALL + " where id = :id") .param("id",id) .query(Kontakt.class) .optional(); }

Einen neuen Kontakt erstellen

Da wir PostgreSQL verwenden können wir die INSERT INTO ... RETURNING COL1,COL2 Syntax verwenden und mit Hilfe eines `KeyHolder' den generierten Schlüssel nach dem Insert zurückzugeben.

@Transactional public Long save(Kontakt kontakt) { String insertSQL = """ INSERT INTO kontakte(bezeichnung,email,telefon,erstellt_am) VALUES (:bezeichnung, :email, :telefon, :erstelltAm) RETURNING id """; KeyHolder keyHolder = new GeneratedKeyHolder(); jdbcClient.sql(insertSQL) .param("bezeichnung", kontakt.bezeichnung()) .param("email", kontakt.email()) .param("telefon", kontakt.telefon()) .param("erstelltAm", Timestamp.from(kontakt.erstelltAm())) .update(keyHolder); return keyHolder.getKeyAs(Long.class); }

Kontakt in der Datenbank ändern

Ein Update könnte folgendermaßen aussehen

@Transactional public void update(Kontakt kontakt) { String updateSQL = """ UPDATE kontakte set bezeichnung=? ,email=? ,telefon=? WHERE id = ? """; final int anzahl = jdbcClient.sql(updateSQL) .param(1, kontakt.bezeichnung()) .param(2, kontakt.email()) .param(3, kontakt.telefon()) .param(4, kontakt.id()) .update(); if(anzahl == 0) { throw new RuntimeException(String.format("Kontaktdateneintrag mit ID %d nicht gefunden!", kontakt.id())); } }

In der update(..)-Methode verwenden wir den Positionsparameter (?) anstatt die Parameter zu benennen. Dies dient nur der Demonstration dieses Features. Mit dem JdbcClient sind also beide Varianten möglich.

Einen Eintrag löschen

Löschen ist auch nicht schwierig

@Transactional public void delete(Long id) { String deleteSQL = """ DELETE FROM kontakte where id = ? """; final int anzahl = jdbcClient.sql(deleteSQL).param(1, id).update(); if(anzahl == 0) { throw new RuntimeException(String.format("Kontaktdateneintrag mit ID %d nicht gefunden!",id)); } }

Test des Repositories mit Testcontainern

Für die Tests sorgen wir dafür das die Datenbank sich immer in einem sauberen bekannten Zustand befindet. Dafür erstellen wir eine Datei src/test/resources/test-data.sql mit dem folgenden Inhalt

TRUNCATE TABLE kontakte; ALTER SEQUENCE kontakte_id_seq RESTART WITH 1; INSERT INTO kontakte (bezeichnung, email, telefon, erstellt_am) values('Wolfgang', 'wolfgang.klaus@muellmail.com', '4711-424242', CURRENT_TIMESTAMP);

Diese Datei fügen wir machen wir mit der Annotation @Sql("/test-data.sql") der Testklasse bekannt. Dieses wird nun vor jedem Test ausgeführt.

package de.saphirgmbh.jdbcexample.domaene; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; import org.springframework.jdbc.core.simple.JdbcClient; import org.springframework.test.context.jdbc.Sql; import java.time.Instant; import java.util.List; import java.util.Optional; import static org.assertj.core.api.Assertions.assertThat; @JdbcTest(properties = { "spring.test.database.replace=none", "spring.datasource.url=jdbc:tc:postgresql:16-alpine:///db" }) @Sql("/test-data.sql") public class KontaktRepositoryTest { @Autowired private JdbcClient jdbcClient; KontaktRepository kontaktRepository; @BeforeEach public void setUp() { kontaktRepository = new KontaktRepository(jdbcClient); } @Test public void sucheAlleKontaktdaten() { final List<Kontakt> kontakte = kontaktRepository.findAll(); Assertions.assertThat(kontakte).isNotEmpty(); Assertions.assertThat(kontakte).hasSize(1); } @Test public void erstelleKontakt() { Kontakt kontakt = new Kontakt(null, "wkl", "wkl@muellmail.com", "4242", Instant.now()); final Long id = kontaktRepository.save(kontakt); assertThat(id).isNotNull(); } @Test public void ermittleKontaktMitId() { Kontakt kontakt = new Kontakt(null, "wkl2", "wkl2@muellmail.com", "4242", Instant.now()); Long id = kontaktRepository.save(kontakt); final Optional<Kontakt> kontaktById = kontaktRepository.findById(id); assertThat(kontaktById).isPresent(); assertThat(kontaktById.get().id()).isEqualTo(id); assertThat(kontaktById.get().bezeichnung()).isEqualTo("wkl2"); } @Test public void keineDatenWennNichtVorhanden() { final Optional<Kontakt> id = kontaktRepository.findById(-9999L); assertThat(id).isNotPresent(); } @Test public void aendereKontakt() { Kontakt kontakt = new Kontakt(null, "wkl3", "wkl3@muellmail.com", "4242", Instant.now()); final Long id = kontaktRepository.save(kontakt); Kontakt geandert= new Kontakt(id, "wkl3", "wkl3@muellmail.com", "4343", kontakt.erstelltAm()); kontaktRepository.update(geandert); final Kontakt geanderterKontakt = kontaktRepository.findById(id).orElseThrow(); assertThat(geanderterKontakt.telefon()).isEqualTo(geandert.telefon()); } @Test public void loescheKontakt() { Kontakt kontakt = new Kontakt(null, "wkl4", "wkl4@muellmail.com", "4444", Instant.now()); final Long id = kontaktRepository.save(kontakt); kontaktRepository.delete(id); final Optional<Kontakt> nichtVorhanden = kontaktRepository.findById(id); assertThat(nichtVorhanden).isNotPresent(); } }

Wir benutzen als JDBC-URL eine spezielle URL um eine PostgreSQL-Datenbank in einem Container zu starten und die Tests gegen diese DB auszuführen.

Fazit

Die neue Klasse JdbcClient bietet eine schöne Fluent-API um Datenbankzugriffe mit JDBC zu implementieren. Die Benutzung des allseits bekannten JdbcTemplate ist auch weiterhin möglich. Ein Blick auf den neuen JdbcClient lohnt dennoch um sauberen leserelichen Code zu erhalten.

← Zurück