days
0
-31
0
hours
0
-9
minutes
-3
-9
seconds
-1
-9
search
Full steam ahead!

Reactive programming with SQL databases: R2DBC 0.8 released

Mark Paluch

© Shutterstock / eamesBo

R2DBC 0.8 is now generally available. R2DBC (Reactive Relational Database Connectivity) is an open initiative that connects reactive programming with SQL databases. The R2DBC working group worked for almost two years before releasing its first version of the specification. Read about the motivation behind R2DBC and its current state.

Reactive programming divides the JVM communities, as it can be a controversial programming model. Reactive programming comes with an incredible boost in scalability with a stream-oriented data flow concept. Entering the reactive space also comes with a steep learning curve, and your code looks entirely different in comparison to imperative code. A key ingredient in a reactive stack is the use of the non-blocking component. In fact, using blocking components in a reactive system can easily break the entire system as a reactive runtime typically runs with a very limited number of threads.

Reactive applications that integrate with SQL databases typically use JDBC, as that is the standard on the JVM for integrating with SQL databases. The use of JDBC enables usage of common frameworks that abstract JDBC away, so applications can focus on the actual requirements instead of technical aspects required by the database communication technology. JDBC is a blocking API. Using JDBC in a reactive application requires offloading of blocking calls on a ThreadPool. The alternative to JDBC is the usage of database- and vendor-specific drivers. Both options result in:

  • Usage of blocking drivers, offloading work onto a ThreadPool while being able to use familiar frameworks
  • Usage of non-blocking drivers without being able to use JDBC frameworks

Both options put application developers in a difficult situation, as none of them is satisfactory. The R2DBC initiative was founded to establish a reactive and standardized API for reactive programming with SQL databases. R2DBC consists of a specification and the R2DBC API. Both artifacts explain how to implement an R2DBC-compliant driver and what framework developers can expect from R2DBC in terms of functionality and behavior. R2DBC creates a foundation for pluggable drivers.

Dependencies

R2DBC used a Java 8 baseline and requires Reactive Streams as an external dependency, as Java 8 has no native reactive programming API. With Java 9, Reactive Streams went into Java itself as Flow API, so future versions of R2DBC can migrate to the Flow API once they target a Java baseline of 9 or higher. Such a move would turn R2DBC into a dependency-free specification.

Structure of R2DBC

R2DBC specifies behavior and a series of base interfaces that are used for the integration between an R2DBC driver and the code that is calling it.

  • ConnectionFactory
  • Connection
  • Statement
  • Result
  • Row

Besides these interfaces, R2DBC comes with a set of categorized exceptions and metadata interfaces to provide details about the driver and database.

The main entry point to a driver is a ConnectionFactory. It creates a Connection that allows interaction with the database.

R2DBC uses Java’s ServiceLoader to discover drivers that are currently on the class path. An application can obtain a ConnectionFactory by providing an R2DBC connection URL:

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

Requesting a Connection starts a non-blocking process to connect to the underlying database. Once connected, the connection can be used to control the transactional state or to run a Statement.

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 and 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();
}

Running a statement results in creating a Result object. A Result returns either the number of affected rows or the rows themselves:

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

Rows are handled in a stream-oriented fashion meaning that rows are emitted once the driver receives and decodes a row from the wire protocol. Consuming a rows requires a mapping function that is applied to each Row. A mapping function can extract any number of values and return either scalar values or a materialized object:

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

Result and 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 is built on Reactive Streams, therefore application code should use a reactive library to consume R2DBC. Bare-bone Publishers are next to unusable directly. Code samples above use Project Reactor.

Scope of the specification

R2DBC specifies how database and JVM types are converted, how R2DBC implementations should behave, and compliance guidelines for an implementation to pass the TCK. R2DBC is, in many areas, inspired by JDBC. That is why using R2DBC feels familiar in several areas. The specification contains:

  • Driver SPI and TCK (Technology Compatibility Kit)
  • Integration with BLOB and CLOB types
  • Plain and Parameterized Statements („Prepared Statements“)
  • Batch operations
  • Categorized Exceptions (R2dbcRollbackException, R2dbcBadGrammarException)
  • ServiceLoader-based Driver Discovery
  • Connection URL scheme

The specification addresses extensions that can be optionally implemented by drivers that want to support these.

R2DBC eco-system

R2DBC started as the specification with the Postgres driver in Spring 2018. After an initial review, the R2DBC working group realized what impact R2DBC could make, and so R2DBC grew into a standard specification. Several projects picked up on R2DBC by providing a driver or a library intended for usage with R2DBC:

Driver

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

Libraries

  • R2DBC Pool (Connection Pool)
  • R2DBC Proxy (Observability Wrapper, similar to P6Spy and DataSource Proxy)

Building an R2DBC driver requires, in most cases, an entirely new implementation of the wire protocol, as most JDBC drivers rely on SocketInputStream and SocketOutputStream. Reimplementation of drivers cause drivers to be very young projects, and drivers should be only used with the appropriate amount of caution. During the Code One conference, Oracle recently presented their plans for the OJDBC 20 driver after announcing that Oracle terminated ADBA efforts. Oracle’s OJDBC20 driver will ship with several reactive extensions that are inspired by ADBA and feedback from the R2DBC working group so that the Oracle driver can be used in reactive applications.

Several database vendors are interested in providing R2DBC drivers.

The same is true for the framework side. Projects such as R2DBC Client, kotysa, and Spring Data R2DBC make R2DBC usable in application code. Other libraries, such as jOOQ, Micronaut, and Hibernate Rx, are aware of R2DBC and eventually want to integrate with R2DBC.

What does the future hold

R2DBC 0.8.0.RELEASE is the first stable release of the open standard. The specification does not stop there: Stored procedures, extensions to transaction definitions, and a specification for database events (such as Postgres Listen/Notify) are only a few topics planned for the upcoming revision R2DBC 0.9.

Author

Mark Paluch

Mark is Software Craftsman, Spring Data Project Lead at Pivotal, and Lead of the Lettuce Redis driver. His focus is now on reactive data integrations and R2DBC.


Leave a Reply

avatar
400