Volle Kraft voraus!

Reaktive Programmierung und SQL: R2DBC 0.8 veröffentlicht

Mark Paluch

© Shutterstock / eamesBo

 

Vor fast zwei Jahren begannen die ersten Arbeiten an der R2DBC-Spezifikation (Reactive Relational Database Connectivity) mit der Idee, SQL-Datenbanken mit reaktiver Programmierung zugänglich zu machen. Nach mehreren Meilensteinen wurde nun die erste Version der Spezifikation nebst einer Reihe von Implementierungen veröffentlicht. Grund genug, sich die Geschichte von R2DBC und den aktuellen Stand der Spezifikation einmal genauer anzuschauen.

Reaktive Programmierung polarisiert die JVM-Welt wie derzeit kein anderes Thema. Auf der einen Seite erhöhen Stream-orientierte Datenflüsse die Skalierbarkeit von Anwendungen erheblich. Auf der anderen Seite sehen sich Entwickler mit einer steilen Lernkurve und einem völlig anderem Programmiermodell konfrontiert. Eine fundamentale Anforderung an die Bausteine reaktiver Programmierung ist, dass sämtliche Komponenten den ausführenden Thread nicht blockieren dürfen. Stattdessen werden non-blocking APIs verwendet, da ansonsten die begrenzten Ressourcen eines reaktiven Systems blockiert werden.

Die bisherigen Ansätze von reaktiven Anwendungen, die auf SQL-Datenbanken setzen, verwenden auf die eine oder andere Weise JDBC, um darauf basierende Bibliotheken nutzen zu können. JDBC ist allerdings ein blockierendes API und erfordert daher das Auslagern der blockierenden Aufrufe auf einem ThreadPool. Alternativen gibt es in Form von Datenbank-spezifischen Treibern, die üblicherweise ein Vendor-spezifisches, asynchrones API bereitstellen. Damit bleibt die folgende Wahl:

  • Blockierende Treiber, dafür die Möglichkeit, gewohnte Frameworks nutzen zu können.
  • Asynchrone Treiber ohne jegliche Frameworkunterstützung.

Aus der Kombination beider Optionen ist die R2DBC-Initiative entstanden. Das Ziel ist, ein reaktives Standard API für SQL-Datenbankintegrationen zu spezifizieren. R2DBC besteht aus einem Spezifikationsdokument und dem R2DBC API, welches Treiberentwickler implementieren und gegen welches Bibliotheken entwickeln, um ein einheitliches API nutzen zu können.

Abhängigkeiten

R2DBC setzt Java 8 voraus und benötigt Reactive Streams als reaktives API, da die Java-Laufzeitumgebung in dieser Version nativ keine Unterstützung für reaktive Programmierung bietet. Das sind die einzigen Abhängigkeiten. Mit Java 9 hat Reactive Streams Einzug in die Java-Bibliotheken in der Form des Flow API gehalten. Daher könnten zukünftige Versionen, die auf Java 9 oder höher setzen, gleich das Flow API verwenden und wären somit komplett frei von externen Abhängigkeiten.

R2DBC-Struktur

R2DBC spezifiziert neben dem Verhalten auch eine Reihe von Basis-Interfaces, die der Interaktion zwischen einem R2DBC-Treiber und dem aufrufenden Code dienen.

  • ConnectionFactory
  • Connection
  • Statement
  • Result
  • Row

Daneben gibt es noch eine Reihe von Exception-Typen und Metadaten-Interfaces, die Auskunft über Ergebnisse oder Treiberdetails liefern. Die ConnectionFactory stellt den Einstieg in einen Treiber da und erzeugt Connections.

Mittels Java ServiceLoader werden verfügbare Treiber im Klassenpfad ermittelt und können per URL-basiertem Lookup konfiguriert werden:

ConnectionFactory connectionFactory = ConnectionFactories
				.get("r2dbc:h2:mem:///my-db?DB_CLOSE_DELAY=-1");
public interface ConnectionFactory {
    Publisher<? extends Connection> create();
    ConnectionFactoryMetadata getMetadata();
}

Sobald eine Connection erzeugt wurde, kann diese verwendet werden, um Transaktionen zu kontrollieren oder Statements auszuführen.

Flux<Result> results = Mono.from(connectionFactory.create()).flatMapMany(connection -> {
			return connection
					.createStatement("CREATE TABLE person (id SERIAL PRIMARY KEY, first_name VARCHAR(255), last_name VARCHAR(255))")
					.execute();
});

Connection und Statement interfaces

public interface Connection extends Closeable {
    Publisher<Void> beginTransaction();
    Publisher<Void> close();
    Publisher<Void> commitTransaction();
    Batch createBatch();
    Publisher<Void> createSavepoint(String name);
    Statement createStatement(String sql);
    boolean isAutoCommit();
    ConnectionMetadata getMetadata();
    IsolationLevel getTransactionIsolationLevel();
    Publisher<Void> releaseSavepoint(String name);
    Publisher<Void> rollbackTransaction();
    Publisher<Void> rollbackTransactionToSavepoint(String name);
    Publisher<Void> setAutoCommit(boolean state);
    Publisher<Void> setTransactionIsolationLevel(IsolationLevel level);
    Publisher<Boolean> validate(ValidationDepth depth);
}

public interface Statement {
    Statement add();
    Statement bind(int index, Object value);
    Statement bind(String name, Object value);
    Statement bindNull(int index , Class<?> type);
    Statement bindNull(String name, Class<?> type);
    Publisher<? extends Result> execute();
}

Result-Objekte repräsentieren das Ergebnis einer Datenbankoperation. Ein Result liefert entweder die Anzahl der betroffenen Zeilen oder die Zeilen selbst:

Flux<Result> results = …;
Flux<Integer> updateCounts = results.flatMap(Result::getRowsUpdated);

Zeilen werden Stream-orientiert konsumiert. Sobald der Treiber eine eingehende Zeile decodiert, wird die Mapping-Funktion auf die Row angewandt, in der einzelne Feldwerte extrahiert werden können. Frameworks können diese Funktionalität nutzen, um Objekte aus Abfrageergebnissen zu materialisieren:

Flux<Result> results = …;
Flux<Integer> updateCounts = results.flatMap(result -> result.map((row, rowMetadata) -> row.get(0, Integer.class)));

Result und Row interfaces

public interface Result {
    Publisher<Integer> getRowsUpdated();
    <T> Publisher<T> map(BiFunction<Row, RowMetadata, ? extends T> mappingFunction);
}

public interface Row {
    Object get(int index);
    <T> T get(int index, Class<T> type);
    Object get(String name);
    <T> T get(String name, Class<T> type);
}

R2DBC basiert auf Reactive Streams, weshalb zur Verwendung eine reaktive Bibliothek erforderlich ist, die auf Reactive Streams setzt. Die vorhergehenden Beispiele nutzen Project Reactor.

Spezifikationsumfang

Die Spezifikation legt fest, wie zwischen Datenbank- und JVM-Typen konvertiert wird, wie sich R2DBC-implementierungen verhalten und welche Teile der Spezifikation implementiert werden müssen, um das TCK erfolgreich zu durchlaufen. Bei genauerem Hinsehen orientiert sich R2DBC stark an JDBC. Die Spezifikation selbst umfasst folgende Punkte:

  • Das Treiber SPI inklusive TCK (Technology Compatibility Kit)
  • Integration mit BLOB- und CLOB-Datentypen
  • Skalare und parametrisierte Statements („Prepared Statements“)
  • Batching
  • Kategorisierte Exceptions (R2dbcRollbackException, R2dbcBadGrammarException)
  • ServiceLoader-basierte Treiberkonfiguration
  • Connection URLs

Darüber hinaus gibt es Extensions, die optional von R2DBC-Treibern implementiert werden können.

R2DBC-Ökosystem

R2DBC ist initial mit der Spezifikation und dem Postgres-Treiber im Frühjahr 2018 gestartet. Es hat sich recht schnell abgezeichnet, dass das Projekt das Potenzial besitzt, zu einem Standard heranzuwachsen. Seit Mitte 2018 sind weitere Treiber und Implementierungen hinzugekommen:

Treiber

  • Google Cloud Spanner
  • H2
  • Microsoft SQL Server
  • MySQL
  • Postgres
  • SAP HANA

Bibliotheken

  • R2DBC Pool (Connection Pool)
  • R2DBC Proxy (Observability Wrapper, ähnlich P6Spy oder DataSource Proxy)

R2DBC erfordert eine Neuimplementierung des Datenbankprotokolls, da die meisten JDBC-Datenbanktreiber auf reguläre SocketInputStream und SocketOutputStream setzen. Daher sind R2DBC-Treiber recht jung und sollten mit Vorsicht eingesetzt werden. Oracle hat seine reaktiven Erweiterungen für den kommenden OJDBC20-Treiber auf der CodeOne-Konferenz vorgestellt. Aus dem stillgelegten ADBA-Vorhaben und dem Feedback der R2DBC-Arbeitsgruppe entstanden im kommenden Oracle-Treiber reaktive APIs, die den Oracle-Treiber im reaktiven Umfeld nutzbar machen.

Weitere Datenbankhersteller sind bereits daran interessiert, R2DBC-Treiber bereitzustellen.

Auf der Integrationsseite gibt es erste Initiativen, R2DBC zu unterstützen. So wurden mit R2DBC Client, kotysa und Spring Data R2DBC die ersten Frameworks angekündigt, die R2DBC unterstützen. Bei weiteren Frameworks wie jOOQ, Micronaut oder Hibernate Rx sind bereits Tickets in den entsprechenden Issue Trackern zu finden.

Ausblick

R2DBC 0.8.0 markiert das erste GA Release in der Geschichte des offenen Standards R2DBC. Auf der Roadmap der Spezifikation stehen bereits Themen für das nächste Release 0.9. Darunter finden sich Unterstützung für Stored Procedures, Erweiterungen für Transaktionsdefinitionen und die Standardisierung für Datenbankevents wie z. B. Postgres Listen/Notify.

Geschrieben von
Mark Paluch
Mark Paluch
Mark Paluch ist Software Craftsman und leitet das Spring-Data-Projekt. Sein Fokus liegt auf reaktiven Treibern und Infrastrukturkomponenten wie R2DBC. Mark ist auch Maintainer des Lettuce-Redis-Treibers.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
4000
  Subscribe  
Benachrichtige mich zu: