HomeAbout
 
  

Spring R2DBC for Reactive Relational Databases in Reactive Programming

February 27, 2023

R2DBC is a framework that provides reactive and non-blocking APIs for relational databases. By using R2DBC (Reactive Relational Database Connectivity), It allows developers to perform read and write operations in a reactive and asynchronous manner within a reactive application.

Not all relational databases support reactive programming. To leverage the benefits of R2DBC, it's important to use a database that has R2DBC support. Otherwise, we would have to resort to other approaches that may not offer the same level of performance and scalability.


In my previous articles, I started from theory and covered topics of reactive endpoints and reactive web client. I suggest you read those to learn the basic concepts of reactive programming and to understand this article better. To comprehend this article, a basic understanding of databases is also required.


When building an application that deals with data, it's essential to have a database in place. However, when it comes to building a reactive application, it's equally important to ensure that the database connection and driver structure are reactive as well. This is necessary to maintain the overall responsiveness and performance of the application.

In the Spring world, the R2DBC structure is used to provide reactivity in the relational database dimension. Currently, driver support is available for Oracle, Microsoft SQL Server, MySQL, PostgreSQL, H2, MariaDB, and Google Cloud Spanner. Today, we will work through examples on the widely used PostgreSQL, but the structure is applicable to all databases with R2DBC support.

Spring R2DBC is not a full ORM like JPA—it does not provide features such as caching and lazy loading and has some limitations. However, it does provide object mapping and abstraction. To put it more clearly, with reactive programming, we can create tables as entities and direct our queries to the database. When it comes to what we can't do, we can't make relationships like OneToMany directly from our classes, meaning that we need to write raw SQL queries or manually establish connections between tables through code, rather than using relational annotations. In other words, having a good understanding of SQL is required when working with R2DBC.


Developing an Application with Spring R2DBC

A spring-boot-starter-data-r2dbc is available to manage the dependencies related to R2DBC. We need to add the dependencies for PostgreSQL database as we will be developing an application using it. You can access all the code on Github."

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-r2dbc'
implementation group: ‘io.r2dbc’, name: ‘r2dbc-postgresql’
implementation group: 'org.springframework.boot', name: 'spring-boot-starter-webflux'

In our project, since we have chosen PostgreSQL, we can configure it accordingly. We can enter all connection settings into our ConnectionFactory by extending our configuration file from AbstractR2dbcConfiguration.

@Configuration
@EnableR2dbcRepositories
public class DatabaseConfig extends AbstractR2dbcConfiguration {
@Override
@Bean
public ConnectionFactory connectionFactory() {
return new PostgresqlConnectionFactory(
PostgresqlConnectionConfiguration.builder()
.host("localhost")
.port(5432)
.username("postgres")
.password("postgres")
.database("mydatabase")
.build());
}
}

Now let's quickly create our tables, and later we can discuss everything we did and why during the entire coding process.

CREATE TABLE address
(
id SERIAL PRIMARY KEY,
street VARCHAR(50) NULL,
city VARCHAR(50) NOT NULL,
state VARCHAR(50)
);
CREATE TABLE reactive_user
(
id serial PRIMARY KEY,
name VARCHAR(50) NOT NULL,
score VARCHAR(50) NOT NULL,
address_id INTEGER REFERENCES address (id)
);
  • In our entity class, the id can be defined as the primary key using the @Id annotation from Spring Data. For those who don't know, this definition means that if the id value is NULL, Spring will create a new record in the table when saving the object. If the id is not NULL, it will try to perform an update operation.
  • There is no @GeneratedValue annotation like in JPA, so the primary key increment is not automatically configured by Spring. We need to manually configure this during the creation of our database.
  • We can also specify that it's a table using the @Table annotation, and by giving a value to it, we can define the name of the table.
  • In the User entity class, we have added a @Transient annotation to the address field to tell Spring Data to ignore this field when mapping to the database.
  • Instead, we have added a new field called "addressId" with the @Column annotation to specify the name of the column in the "reactive_user" table that corresponds to the Address entity's primary key. This will allow us to establish a one-to-one relationship between the User and Address entities.
@Table("reactive_user")
public class User {
@Id
private Integer id;
private String name;
private Integer score;
@Column("address_id")
private Long addressId;
@Transient
private Address address;
// Getters, Setters ...
@Table("address")
public class Address {
@Id
private Long id;
private String street;
private String city;
private String state;
// Getters, Setters ...

Now we can say that our entity corresponding to the table is ready. Now it's time to create a repository that can communicate with the database and write our Queries. The first step here is to create a UserRepository interface by extending a ReactiveCrudRepository and using ReactiveCrudRepository to create a reactive repository. Using this repository, we can use the existing methods (such as findAll, findById, save and delete) or write new Queries as shown in the example.

public interface UserRepository extends ReactiveCrudRepository<User, Long> {
@Query("select id,name,score from reactive_user where name=$1")
Flux<User> findByName(String name);
Mono<User> findById(int id);
Mono<Void> deleteById(int id);
}
@Repository
public interface AddressRepository extends ReactiveCrudRepository<Address, Long> {
}

Additionally, to execute more specific or complex queries, we can use the @Query annotation, allowing us to write raw SQL queries.

Now let's prepare our service and connect our repositories, which can communicate with our database, to the reactive endpoints we previously wrote, creating a real service. Here, we need to inject our repository into our service.

@Service
public class UserServiceImpl implements UserService {
private final UserWebClient userWebClient;
private final UserRepository userRepository;
private final AddressRepository addressRepository;
public UserServiceImpl(UserWebClient userWebClient, UserRepository userRepository, AddressRepository addressRepository) {
this.userWebClient = userWebClient;
this.userRepository = userRepository;
this.addressRepository = addressRepository;
}
@Override
public Mono<User> getUserById(int id) {
return userRepository.findById(id)
.switchIfEmpty(Mono.error(new NotFoundException("User not found")))
.flatMap(user -> {
if (user.getAddressId() == null) {
return Mono.just(user);
}
return addressRepository.findById(user.getAddressId()).map(address -> {
user.setAddress(address);
return user;
});
});
}

This method retrieves a user by their ID from the database. The method starts by calling the findById method on the userRepository and passing in the user ID. If the user is not found, the switchIfEmpty operator is used to return a custom error message wrapped in a Mono.error instance.

If the user is found, the flatMap operator is used to check whether the user has an associated address. If the user has no address, a Mono.just operator is used to return the user as is. If the user has an address, the findById method is called on the addressRepository with the address ID. The result is then mapped to update the user object with the corresponding address object using the map operator. Finally, the updated user object is returned.

@Override
public Mono<User> saveUser(User userDTO) {
if (userDTO.getAddress() == null) return userRepository.save(userDTO);
return saveUserWithAddress(userDTO);
}
public Mono<User> saveUserWithAddress(User user) {
return addressRepository.save(user.getAddress()).flatMap(address -> {
user.setAddressId(address.getId());
return userRepository.save(user);
});
}

The saveUser method saves a new user to the database. If the userDTO does not have an associated address, the userRepository's save method is called to save the user object directly to the database.

If the userDTO has an associated address, the saveUserWithAddress method is called. In this method, the address is first saved to the database using the addressRepository's save method. Once the address is saved, the ID of the new address object is set in the user object using setAddressId. Finally, the userRepository's save method is called to save the user object, which now has an associated address ID.


Compared to JPA applications, using R2DBC requires a lot more manual work in the ORM layer by developers rather than relying on the framework. Currently, I do not think R2DBC is ready for use in production for complex applications, but it can be used experimentally or for microservices. However, Spring is working on improvements in this area and I believe they will be ready for reactive databases in no time. Reactive programming is becoming a trend and learning about reactive programming is beneficial, as it is a growing trend and a leading topic for the future.

You can access all the code from the Github repository.

 

Gökhan Ayrancıoğlu

Gökhan Ayrancıoğlu

Sr. Software Engineer @Heycar | Tech Blogger

 

Copyright © 2023 All rights reserved

Made with Coffee ☕