1. Introduction
R2DBC (Reactive Relational Database Connectivity) is an effort presented by Pivotal during Spring One Platform 2018. It intends to create a reactive API to SQL databases.
2. Our First Spring Data R2DBC Project
<dependencies>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>io.r2dbc</groupId>
<artifactId>r2dbc-h2</artifactId>
</dependency>
</dependencies>
Other required artifacts include Lombok, Spring WebFlux and a few others that complete our project dependencies.
3. Connection Factory
When working with a database, we need a connection factory. So, of course, we’ll need the same thing with R2DBC.
So we’ll now add the details to connect to our instance:
@Configuration
@EnableR2dbcRepositories
class R2DBCConfiguration extends AbstractR2dbcConfiguration {
@Bean
public H2ConnectionFactory connectionFactory() {
return new H2ConnectionFactory(
H2ConnectionConfiguration.builder()
.url("mem:testdb;DB_CLOSE_DELAY=-1;")
.username("sa")
.build()
);
}
}
The first thing we notice in the code above is the @EnableR2dbcRepositories. We need this annotation to use Spring Data functionality. In addition, we’re extending the AbstractR2dbcConfiguration since it’ll provide a lot of beans that we’d need later on.
4. Our First R2DBC Application
Our next step is to create the repository:
interface PlayerRepository extends ReactiveCrudRepository<Player, Integer> {}
The ReactiveCrudRepository interface is very useful. It provides, for example, basic CRUD functionality.
Finally, we’ll define our model class. We’ll use Lombok to avoid boilerplate code:
@Data
@NoArgsConstructor
@AllArgsConstructor
class Player {
@Id
Integer id;
String name;
Integer age;
}
5. Testing
It’s time to test our code. So, let’s start by creating a few test cases:
@Test
public void whenDeleteAll_then0IsExpected() {
playerRepository.deleteAll()
.as(StepVerifier::create)
.expectNextCount(0)
.verifyComplete();
}
@Test
public void whenInsert6_then6AreExpected() {
insertPlayers();
playerRepository.findAll()
.as(StepVerifier::create)
.expectNextCount(6)
.verifyComplete();
}
6. Custom Queries
We can also generate custom queries. In order to add it, we’ll need to change our PlayerRepository:
@Query("select id, name, age from player where name = $1")
Flux<Player> findAllByName(String name);
@Query("select * from player where age = $1")
Flux<Player> findByAge(int age);
In addition to the existing tests, we’ll add tests to our recently updated repository:
@Test
public void whenSearchForCR7_then1IsExpected() {
insertPlayers();
playerRepository.findAllByName("CR7")
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
@Test
public void whenSearchFor32YearsOld_then2AreExpected() {
insertPlayers();
playerRepository.findByAge(32)
.as(StepVerifier::create)
.expectNextCount(2)
.verifyComplete();
}
private void insertPlayers() {
List<Player> players = Arrays.asList(
new Player(1, "Kaka", 37),
new Player(2, "Messi", 32),
new Player(3, "Mbappé", 20),
new Player(4, "CR7", 34),
new Player(5, "Lewandowski", 30),
new Player(6, "Cavani", 32)
);
playerRepository.saveAll(players).subscribe();
}
7. R2dbcEntityTemplate
Spring Boot 2.4 introduced the R2dbcEntityTemplate class to perform common R2DBC operations. It provides a fluent API for executing CRUD operations.
To use the class, we create a ConnectionFactory object to establish a connection to the H2 database:
ConnectionFactory connectionFactory = ConnectionFactories.get(
"r2dbc:h2:mem:///testdb?options=DB_CLOSE_DELAY=-1;TRACE_LEVEL_FILE=4;USER=sa;PASSWORD="
);
We can then inject R2dbcEntityTemplate into the test class. The constructor accepts the ConnectionFactory that we just created as an argument:
R2dbcEntityTemplate template = new R2dbcEntityTemplate(connectionFactory);
Now, we can use the template to populate the database with some test data:
void insertPlayers() {
List<Player> players = Arrays.asList(
new Player(null, "Saka", 22),
new Player(null, "Pedro", 32),
new Player(null, "Mbappé", 20)
);
for (Player player : players) {
template.insert(Player.class)
.using(player)
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
}
Here, we invoke the insert() method on the template object to initiate an insert operation. Finally, we use the using() method, which accepts the Player object as a parameter, to insert one object at a time:
Next, let’s create a test method that gets all players from the database:
@Test
void whenInsertThreePlayers_thenThreeAreExpected() {
insertPlayers();
template.select(Player.class)
.all()
.as(StepVerifier::create)
.expectNextCount(3)
.verifyComplete();
}
In the code above, we select the Player entity using the select() method. Then, we invoke all() on it to get all the players in the database. Finally, we assert that the total number of players equals 3.
Finally, let’s perform a custom query by invoking the match() method on the template object:
@Test
void whenSearchForSaka_thenOneIsExpected() {
insertPlayers();
template.select(Player.class)
.matching(query(where("name").is("Saka")))
.one()
.as(StepVerifier::create)
.expectNextCount(1)
.verifyComplete();
}
In the code above, we select the Player entity and invoke the matching() method on it. Furthermore, we write a query to get a player based on name.
In this case, we’re expecting one result and we assert that the return count is exactly 1.
Notably, we can do more with R2dbcTemplate like perform delete and update operations.
8. Batches
Another feature of R2DBC is to create batches. A batch is useful when executing multiple SQL statements as they’ll perform better than individual operations.
To create a Batch we need a Connection object:
Batch batch = connection.createBatch();
After our application creates the Batch instance, we can add as many SQL statements as we want. To execute it, we’ll invoke the execute() method. The result of a batch is a Publisher that’ll return a result object for each statement.
So let’s jump into the code and see how we can create a Batch:
@Test
public void whenBatchHas2Operations_then2AreExpected() {
Mono.from(factory.create())
.flatMapMany(connection -> Flux.from(connection
.createBatch()
.add("select * from player")
.add("select * from player")
.execute()))
.as(StepVerifier::create)
.expectNextCount(2)
.verifyComplete();
}
9. Conclusion
To summarize, R2DBC makes it easier to build Spring-powered applications that use relational data access technologies in a reactive application stack. It’s an attempt to create an SPI that will define a reactive API to SQL databases. When used with Spring WebFlux, R2DBC allows us to write an application that handles data asynchronously from the top and all the way down to the database.
As always the code is available at GitHub.